From 6e17a8d6d6213c883c672dd5906966c1cace592c Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Sun, 19 May 2024 22:22:41 +0200 Subject: [PATCH 01/28] feat(utils): impl chacha for compat with rand_chacha --- src/lib.zig | 1 + src/utils/chacha.zig | 542 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 543 insertions(+) create mode 100644 src/utils/chacha.zig diff --git a/src/lib.zig b/src/lib.zig index 1ada9cb91..b524ee481 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -63,6 +63,7 @@ pub const sync = struct { pub const utils = struct { pub usingnamespace @import("utils/arraylist.zig"); pub usingnamespace @import("utils/bitflags.zig"); + pub usingnamespace @import("utils/chacha.zig"); pub usingnamespace @import("utils/lazy.zig"); pub usingnamespace @import("utils/math.zig"); pub usingnamespace @import("utils/shortvec.zig"); diff --git a/src/utils/chacha.zig b/src/utils/chacha.zig new file mode 100644 index 000000000..ce3e857e8 --- /dev/null +++ b/src/utils/chacha.zig @@ -0,0 +1,542 @@ +//! Port of ChaCha from the `rand_chacha` crate. +//! Generates the same psuedorandom numbers as rand_chacha, +//! unlike Zig std's ChaCha. This is needed since rand_chacha +//! does not comply with the IETF standard. + +const std = @import("std"); +const builtin = @import("builtin"); +const sig = @import("../lib.zig"); + +const mem = std.mem; + +const Return = sig.utils.Return; + +const BUFSZ: usize = 64; +const endian = builtin.cpu.arch.endian(); + +/// A random number generator based on ChaCha. +/// Generates the same stream as `rand_chacha` +pub fn ChaChaRng(comptime rounds: usize) type { + return BlockRng(ChaCha(rounds), ChaCha(rounds).generate); +} + +/// Wrapper for random number generators which generate +/// blocks of [64]u32. Minimizes calls to the underlying +/// random number generator by recycling unused data from +/// previous calls. Port of BlockRng from rust which +/// ensures the same sequence is generated. +pub fn BlockRng( + comptime T: type, + comptime generate: fn (*T, *[64]u32) void, +) type { + return struct { + results: [64]u32 = undefined, + index: usize = 64, + core: T, + + const Self = @This(); + + pub fn init(seed: anytype) Self { + return .{ .core = @call(.auto, T.init, .{seed}) }; + } + + pub fn random(self: *Self) std.rand.Random { + return std.rand.Random.init(self, fill); + } + + pub fn fill(self: *Self, dest: []u8) void { + var completed_bytes: usize = 0; + while (completed_bytes < dest.len) { + if (self.index >= self.results.len) { + generate(&self.core, &self.results); + self.index = 0; + } + const src: [*]u8 = @ptrCast(self.results[self.index..].ptr); + const num_u8s = @min(4 * (64 - self.index), dest.len - completed_bytes); + @memcpy(dest[completed_bytes..][0..num_u8s], src[0..num_u8s]); + + self.index += (num_u8s + 3) / 4; + completed_bytes += num_u8s; + } + } + }; +} + +/// Computes the chacha stream. +/// +/// This is the underlying implementation of the chacha cipher. +/// If you're looking for a random number generator, ChaChaRng +/// is a better candidate. +pub fn ChaCha(rounds: usize) type { + return struct { + b: [4]u32, + c: [4]u32, + d: [4]u32, + + const Self = @This(); + + pub fn init(seed: [32]u8) Self { + return Self.initWithNonce(seed, .{0} ** 12); + } + + pub fn initWithNonce(key: [32]u8, nonce: [12]u8) Self { + const ctr_nonce = .{0} ++ leIntBitCast([3]u32, nonce); + return .{ + .b = leIntBitCast([4]u32, key[0..16].*), + .c = leIntBitCast([4]u32, key[16..].*), + .d = ctr_nonce, + }; + } + + /// Run the full chacha algorithm, generating the next block + /// of 64 32-bit integers. + pub fn generate(self: *Self, out: *[64]u32) void { + const k = [4]u32{ 0x6170_7865, 0x3320_646e, 0x7962_2d32, 0x6b20_6574 }; + const b = self.b; + const c = self.c; + var x = State{ + .a = .{ k, k, k, k }, + .b = .{ b, b, b, b }, + .c = .{ c, c, c, c }, + .d = repeatx4AndAdd0123(self.d), + }; + for (0..rounds / 2) |_| { + x = diagonalize(round(diagonalize(round(x), 1)), -1); + } + const sb = self.b; + const sc = self.c; + const sd = repeatx4AndAdd0123(self.d); + const results: [64]u32 = @bitCast(transpose4(.{ + wrappingAddEachInt(x.a, .{ k, k, k, k }), + wrappingAddEachInt(x.b, .{ sb, sb, sb, sb }), + wrappingAddEachInt(x.c, .{ sc, sc, sc, sc }), + wrappingAddEachInt(x.d, sd), + })); + @memcpy(out[0..64], &results); + self.d = wrappingAddToFirstHalf(sd[0], 4); + } + }; +} + +const State = struct { + a: [4][4]u32, + b: [4][4]u32, + c: [4][4]u32, + d: [4][4]u32, + + const Self = @This(); + + fn eql(self: *const Self, other: *const Self) bool { + inline for (.{ "a", "b", "c", "d" }) |field_name| { + const lhs = @field(self, field_name); + const rhs = @field(other, field_name); + for (0..4) |i| { + if (!mem.eql(u32, &lhs[i], &rhs[i])) return false; + } + } + return true; + } +}; + +fn transpose4(a: [4][4][4]u32) [4][4][4]u32 { + return .{ + .{ a[0][0], a[1][0], a[2][0], a[3][0] }, + .{ a[0][1], a[1][1], a[2][1], a[3][1] }, + .{ a[0][2], a[1][2], a[2][2], a[3][2] }, + .{ a[0][3], a[1][3], a[2][3], a[3][3] }, + }; +} + +/// converts the first two items into a u64 and +/// then wrapping_adds the integer `i` to it, +/// then converts back to u32s. +fn wrappingAddToFirstHalf(d: [4]u32, i: u64) [4]u32 { + var u64s = leIntBitCast([2]u64, d); + u64s[0] += i; + return leIntBitCast([4]u32, u64s); +} + +fn repeatx4AndAdd0123(d: [4]u32) [4][4]u32 { + return .{ + wrappingAddToFirstHalf(d, 0), + wrappingAddToFirstHalf(d, 1), + wrappingAddToFirstHalf(d, 2), + wrappingAddToFirstHalf(d, 3), + }; +} + +/// Run a single round of the ChaCha algorithm +fn round(state: State) State { + var x = state; + x.a = wrappingAddEachInt(x.a, x.b); + x.d = xorThenRotateRight(x.d, x.a, 16); + x.c = wrappingAddEachInt(x.c, x.d); + x.b = xorThenRotateRight(x.b, x.c, 20); + x.a = wrappingAddEachInt(x.a, x.b); + x.d = xorThenRotateRight(x.d, x.a, 24); + x.c = wrappingAddEachInt(x.c, x.d); + x.b = xorThenRotateRight(x.b, x.c, 25); + return x; +} + +fn wrappingAddEachInt(a: [4][4]u32, b: [4][4]u32) [4][4]u32 { + var sum: [4][4]u32 = undefined; + for (0..4) |i| for (0..4) |j| { + sum[i][j] = a[i][j] +% b[i][j]; + }; + return sum; +} + +fn xorThenRotateRight(const_lhs: [4][4]u32, rhs: [4][4]u32, rotate: anytype) [4][4]u32 { + var lhs = const_lhs; + for (0..4) |i| for (0..4) |j| { + const xor = lhs[i][j] ^ rhs[i][j]; + lhs[i][j] = std.math.rotr(u32, xor, rotate); + }; + return lhs; +} + +/// Reinterprets an integer or array of integers as an +/// integer or array of integers with different sizes. +/// For example, can convert u64 -> [2]u32 or vice versa. +/// +/// The function ensures that the resulting numbers are +/// universal across platforms, using little-endian ordering. +/// +/// So, this is the same as @bitCast for little endian platforms, +/// but it requires a byte swap for big endian platforms. +fn leIntBitCast(comptime Output: type, input: anytype) Output { + switch (endian) { + .little => return @bitCast(input), + .big => { + if (numItems(Output) > numItems(@TypeOf(input))) { + var in = input; + for (&in) |*n| n.* = @byteSwap(n); + return @bitCast(in); + } else { + var out: Output = @bitCast(input); + for (&out) |*n| n.* = @byteSwap(n); + return out; + } + }, + } +} + +/// len of array, or 1 if not array. +fn numItems(comptime T: type) usize { + return switch (@typeInfo(T)) { + .Array => |a| a.len, + else => 1, + }; +} + +fn diagonalize(x: State, times: isize) State { + var out: State = x; + for (0..4) |i| { + out.b[i] = rotateLeft(x.b[i], 1 * times); + out.c[i] = rotateLeft(x.c[i], 2 * times); + out.d[i] = rotateLeft(x.d[i], 3 * times); + } + return out; +} + +/// Rotates array items to different locations in the array. +fn rotateLeft(item: [4]u32, n: isize) [4]u32 { + return .{ + item[mod(n, 4)], + item[mod((n + 1), 4)], + item[mod((n + 2), 4)], + item[mod((n + 3), 4)], + }; +} + +fn mod(n: isize, len: usize) usize { + return @intCast(std.math.mod(isize, n, @intCast(len)) catch unreachable); +} + +test "Random.int(u32) works" { + const chacha = ChaCha(20).init(.{0} ** 32); + var rng = BlockRng(ChaCha(20), ChaCha(20).generate){ .core = chacha }; + const random = rng.random(); + try std.testing.expect(2917185654 == random.int(u32)); +} + +test "Random.int(u64) works" { + const chacha = ChaCha(20).init(.{0} ** 32); + var rng = BlockRng(ChaCha(20), ChaCha(20).generate){ .core = chacha }; + const random = rng.random(); + try std.testing.expect(10393729187455219830 == random.int(u64)); +} + +test "Random.bytes works" { + const chacha = ChaCha(20).init(.{0} ** 32); + var rng = BlockRng(ChaCha(20), ChaCha(20).generate){ .core = chacha }; + const random = rng.random(); + var dest: [32]u8 = undefined; + const midpoint = .{ + 118, 184, 224, 173, 160, 241, 61, 144, 64, 93, 106, 229, 83, 134, 189, 40, 189, 210, + 25, 184, 160, 141, 237, 26, 168, 54, 239, 204, 139, 119, 13, 199, + }; + random.bytes(&dest); + try std.testing.expect(mem.eql(u8, &midpoint, &dest)); +} + +test "recursive fill" { + var bytes: [32]u8 = .{0} ** 32; + var rng_init = ChaChaRng(20).init(bytes); + rng_init.fill(&bytes); + const chacha = ChaCha(20).init(bytes); + var rng = BlockRng(ChaCha(20), ChaCha(20).generate){ .core = chacha }; + rng.fill(&bytes); + + const expected = .{ + 176, 253, 20, 255, 150, 160, 189, 161, 84, 195, 41, 8, 44, 156, 101, 51, 187, + 76, 148, 115, 191, 93, 222, 19, 143, 130, 201, 172, 85, 83, 217, 88, + }; + try std.testing.expect(mem.eql(u8, &expected, &bytes)); +} + +test "dynamic next int works" { + var bytes: [32]u8 = .{0} ** 32; + var rng_init = ChaChaRng(20).init(bytes); + rng_init.fill(&bytes); + const chacha = ChaCha(20).init(bytes); + var rng = BlockRng(ChaCha(20), ChaCha(20).generate){ .core = chacha }; + const u32s = [_]u32{ + 4279565744, 862297132, 2898887311, 3678189893, 3874939098, 1553983382, 1031206440, + 978567423, 4209765794, 2063739027, 3497840189, 3042885724, 13559713, 2804739726, + 83427940, 1888646802, 2860787473, 1877744140, 3871387528, 2786522908, 315930854, + 120980593, 3002074910, 3285478202, 1586689760, 2340124627, 52115417, 2748045760, + 3357889967, 214072547, 1511164383, 1921839307, 842278728, 1023471299, 3744819639, + 4085269185, 3222055698, 1508829632, 3587328034, 451202787, 3647660313, 3102981063, + 3964799389, 3904121230, 2805919233, 2118987761, 3557954211, 3320127761, 2756534424, + 992375503, 3545628137, 1085584675, 1209223666, 2255867162, 1635202960, 2496462192, + 713473244, 1792112125, 3844522849, 2490299132, 4072683334, 70142460, 2095023485, + 461018663, 3859958840, 212748047, 2657724434, 81297974, 3942098154, 958741438, + 346419548, 2225828352, 2900251414, 336469631, 654063680, 1812174127, 609007208, + 846863059, 3189927372, 1905581022, 2172277675, 4037927613, 3495064163, 3874649746, + 3559563381, 590810202, 2664210773, 3223769241, 2040745611, 360514407, 2919944502, + 536370302, 1065703962, 7253915, 337567527, 1460887337, 1474807598, 1848190485, + 4096711861, 3404804800, + }; + const u64s = [_]u64{ + 588215994606788758, 1431684808409631931, 2931207493295625045, 3032891578644758194, + 418541299932196982, 15396241028896397909, 12835735727689995230, 9873538067654336105, + 12514793613430075092, 13232023512861211064, 16028062863378687135, 16967702477157236558, + 2887555945435226072, 17400462721248040447, 17117735807058458868, 15659305100245141846, + 2699089690138758189, 10755240647284155175, 1924095871294250220, 17515951820211362774, + 13373595865079936501, 6860971170011447095, 14703706923713349358, 11533069247885725721, + 3448216242831738015, 9278269248351279695, 9372255405263360037, 8707387524018776887, + 8746597495079144985, 7691371180483864508, 7537003725416187104, 1981019672903425841, + 10056652572362307735, 2436364459124478962, 2428925607328334081, 14712031039183662158, + 2614237173466617322, 4257610326057511672, 3540403114074859660, 6581767110215406295, + 15150451542146080734, 181278900145439701, 11760969932321600702, 17522913230875340068, + 10318893824576666810, 18312828410504980228, 2805875854392392082, 5355795946829941939, + 7515894275194643237, 9702265981800844421, 227603388627345368, 3324436570698804108, + 4753191896749056049, 17885086500265945805, 17435295308389799126, 5786986546027884036, + 17350667365223054483, 1154396925486892856, 5844933381342596954, 9570272635503767656, + 16336838788699700779, 2336639497643599348, 9795949699684750554, 6329973578295938791, + 15992525826554723486, 17793526484350803500, 13898491381782030824, 4397579918151967336, + 17917727240936500825, 7352683368508344350, 11766507471434633205, 9634720798753459106, + 16282012887761187213, 16324707443307008843, 14425283330535396682, 13172406095143567691, + 2691725161073047006, 1406030345077942778, 9684222056303881176, 9746143945091321583, + 8181709559804695063, 1654050647849141241, 18149780750595962095, 8493844361058276091, + 9446739672321797014, 12390809841934868939, 15188448811864282367, 98895932768533343, + 5024754166561341894, 9730267002865676284, 11893802928445802006, 18309480227270911117, + 17066717792185926269, 13499718013438346758, 5217404074882333630, 12694155839474838416, + 3008502677940577076, 11542601400063272771, 1730084963375478886, 1114921244491478328, + }; + var random = rng.random(); + for (0..100) |i| { + try std.testing.expect(u32s[i] == random.int(u32)); + try std.testing.expect(u64s[i] == random.int(u64)); + } +} + +test "rng works" { + const chacha = ChaCha(20).init(.{0} ** 32); + var rng = BlockRng(ChaCha(20), ChaCha(20).generate){ .core = chacha }; + var dest: [32]u8 = undefined; + const midpoint = .{ + 118, 184, 224, 173, 160, 241, 61, 144, 64, 93, 106, 229, 83, 134, 189, 40, 189, 210, + 25, 184, 160, 141, 237, 26, 168, 54, 239, 204, 139, 119, 13, 199, + }; + rng.fill(&dest); + // assert_eq!(midpoint, dest); + try std.testing.expect(mem.eql(u8, &midpoint, &dest)); +} + +test "ChaCha works" { + var chacha = ChaCha(20){ + .b = .{ 1, 2, 3, 4 }, + .c = .{ 5, 6, 7, 8 }, + .d = .{ 9, 10, 11, 12 }, + }; + var out: [64]u32 = .{0} ** 64; + chacha.generate(&out); + const expected1 = .{ 514454965, 2343183702, 485828088, 2392727011, 3682321578, 3166467596, 1535089427, 266038024, 1861812015, 3818141583, 486852448, 277812666, 1961317633, 3870259557, 3811097870, 10333140, 3471107314, 854767140, 1292362001, 1791493576, 684928595, 2735203077, 3103536681, 1555264764, 2953779204, 1335099419, 3308039343, 3071159758, 676902921, 3409736680, 289978712, 198159109, 4106483464, 4193260066, 389599996, 1248502515, 607568078, 3047265466, 2254027974, 3837112036, 2647654845, 3933149571, 251366014, 192741632, 4239604811, 2829206891, 2090618058, 86120867, 3489155609, 162839505, 3738605468, 1369674854, 3501711964, 3507855056, 3021042483, 747171775, 3095039326, 1302941762, 1534526601, 4269591531, 2416037718, 2139104272, 3631556128, 4065100274 }; + try std.testing.expect(mem.eql(u32, &expected1, &out)); +} + +/// for testing +const test_start_state = State{ + .a = .{ .{ 0, 1, 2, 3 }, .{ 4, 5, 6, 7 }, .{ 8, 9, 10, 11 }, .{ 12, 13, 14, 15 } }, + .b = .{ + .{ 16, 17, 18, 19 }, + .{ 20, 21, 22, 23 }, + .{ 24, 25, 26, 27 }, + .{ 28, 29, 30, 31 }, + }, + .c = .{ + .{ 32, 33, 34, 35 }, + .{ 36, 37, 38, 39 }, + .{ 40, 41, 42, 43 }, + .{ 44, 45, 46, 47 }, + }, + .d = .{ + .{ 48, 49, 50, 51 }, + .{ 52, 53, 54, 55 }, + .{ 56, 57, 58, 59 }, + .{ 60, 61, 62, 63 }, + }, +}; + +test "d0123 works" { + const input = .{ 1, 2, 3, 4 }; + const expected_out: [4][4]u32 = .{ + .{ 1, 2, 3, 4 }, + .{ 2, 2, 3, 4 }, + .{ 3, 2, 3, 4 }, + .{ 4, 2, 3, 4 }, + }; + const output = repeatx4AndAdd0123(input); + for (0..4) |i| { + try std.testing.expect(mem.eql(u32, &expected_out[i], &output[i])); + } +} + +test "diagonalize round trip" { + const mid = diagonalize(test_start_state, 1); + const expected_mid = State{ + .a = .{ + .{ 0, 1, 2, 3 }, + .{ 4, 5, 6, 7 }, + .{ 8, 9, 10, 11 }, + .{ 12, 13, 14, 15 }, + }, + .b = .{ + .{ 17, 18, 19, 16 }, + .{ 21, 22, 23, 20 }, + .{ 25, 26, 27, 24 }, + .{ 29, 30, 31, 28 }, + }, + .c = .{ + .{ 34, 35, 32, 33 }, + .{ 38, 39, 36, 37 }, + .{ 42, 43, 40, 41 }, + .{ 46, 47, 44, 45 }, + }, + .d = .{ + .{ 51, 48, 49, 50 }, + .{ 55, 52, 53, 54 }, + .{ 59, 56, 57, 58 }, + .{ 63, 60, 61, 62 }, + }, + }; + try std.testing.expect(expected_mid.eql(&mid)); + const end = diagonalize(mid, -1); + try std.testing.expect(test_start_state.eql(&end)); +} + +test "round works" { + const expected = State{ + .a = .{ + .{ 196626, 805502996, 1610809366, 1342373912 }, + .{ 3221422106, 4026728476, 2684551198, 2416115744 }, + .{ 2147680289, 2952986659, 3758293029, 3489857575 }, + .{ 1073938473, 1879244843, 537067565, 268632111 }, + }, + .b = .{ + .{ 2441679121, 269101448, 2458599458, 319568059 }, + .{ 2542629751, 370052078, 2492424772, 353393373 }, + .{ 2375079117, 202501204, 2391999998, 252968295 }, + .{ 2341779115, 169201202, 2291574680, 152542977 }, + }, + .c = .{ + .{ 589304352, 539169873, 623253122, 639965299 }, + .{ 791419620, 741285141, 690626246, 707338423 }, + .{ 454566312, 404431833, 488515082, 505227259 }, + .{ 387197292, 337062813, 286403918, 303116095 }, + }, + .d = .{ + .{ 587207168, 536876080, 620762720, 637540432 }, + .{ 788536000, 738204912, 687873696, 704651408 }, + .{ 452993408, 402662320, 486548960, 503326672 }, + .{ 385886528, 335555440, 285224224, 302001936 }, + }, + }; + try std.testing.expect(expected.eql(&round(test_start_state))); +} + +test "bitcast works as vec128" { + const gas = [4]u32{ + std.math.maxInt(u32) / 2, + std.math.maxInt(u32) / 5, + std.math.maxInt(u32) / 7, + std.math.maxInt(u32) / 11, + }; + const liquid: [2]u64 = @bitCast(gas); + const solid: u128 = @bitCast(gas); + const expected_liquid: [2]u64 = .{ 3689348816030400511, 1676976733025356068 }; + const expected_solid = 30934760611684291960695475747055206399; + try std.testing.expect(mem.eql(u64, &expected_liquid, &liquid)); + try std.testing.expect(expected_solid == solid); + try std.testing.expect(mem.eql(u32, &gas, &@as([4]u32, @bitCast(liquid)))); + try std.testing.expect(mem.eql(u32, &gas, &@as([4]u32, @bitCast(solid)))); +} + +test "rotate_right" { + const start = [4]u32{ 16, 17, 18, 19 }; + inline for (.{ + .{ 0, .{ 16, 17, 18, 19 } }, + .{ 1, .{ 8, 2147483656, 9, 2147483657 } }, + .{ 16, .{ 1048576, 1114112, 1179648, 1245184 } }, + .{ 29, .{ 128, 136, 144, 152 } }, + .{ 64, .{ 16, 17, 18, 19 } }, + }) |x| { + const n, const expected = x; + inline for (0..4) |i| { + const start_item = start[i]; + // const right = n % 32; + // const left: u32 = (32 - right) % 32; + // const out = start_item << left | start_item >> @intCast(right); + try std.testing.expect(expected[i] == std.math.rotr(u32, start_item, n)); + } + } +} + +test "add_pos works" { + const input = .{ 1, 2, 3, 4 }; + const i = 1892390; + const output = wrappingAddToFirstHalf(input, i); + try std.testing.expect(mem.eql(u32, &[4]u32{ 1892391, 2, 3, 4 }, &output)); +} + +test "transpose works" { + const input = [4][4][4]u32{ + .{ .{ 0, 1, 2, 3 }, .{ 4, 5, 6, 7 }, .{ 8, 9, 10, 11 }, .{ 12, 13, 14, 15 } }, + .{ .{ 16, 17, 18, 19 }, .{ 20, 21, 22, 23 }, .{ 24, 25, 26, 27 }, .{ 28, 29, 30, 31 } }, + .{ .{ 32, 33, 34, 35 }, .{ 36, 37, 38, 39 }, .{ 40, 41, 42, 43 }, .{ 44, 45, 46, 47 } }, + .{ .{ 48, 49, 50, 51 }, .{ 52, 53, 54, 55 }, .{ 56, 57, 58, 59 }, .{ 60, 61, 62, 63 } }, + }; + const actual = transpose4(input); + const expected: [4][4][4]u32 = .{ + .{ .{ 0, 1, 2, 3 }, .{ 16, 17, 18, 19 }, .{ 32, 33, 34, 35 }, .{ 48, 49, 50, 51 } }, + .{ .{ 4, 5, 6, 7 }, .{ 20, 21, 22, 23 }, .{ 36, 37, 38, 39 }, .{ 52, 53, 54, 55 } }, + .{ .{ 8, 9, 10, 11 }, .{ 24, 25, 26, 27 }, .{ 40, 41, 42, 43 }, .{ 56, 57, 58, 59 } }, + .{ .{ 12, 13, 14, 15 }, .{ 28, 29, 30, 31 }, .{ 44, 45, 46, 47 }, .{ 60, 61, 62, 63 } }, + }; + for (0..4) |i| for (0..4) |j| { + try std.testing.expect(mem.eql(u32, &expected[i][j], &actual[i][j])); + }; +} From 25a65e218643771255c2e1011f13c6b38fc3e368 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 21 May 2024 17:35:03 -0500 Subject: [PATCH 02/28] feat(rand): WeightedRandomSampler and reorganize rand --- src/lib.zig | 5 ++ src/{utils => rand}/chacha.zig | 55 ++---------- src/rand/rand.zig | 151 +++++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+), 48 deletions(-) rename src/{utils => rand}/chacha.zig (91%) create mode 100644 src/rand/rand.zig diff --git a/src/lib.zig b/src/lib.zig index b524ee481..eee614eff 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -73,6 +73,11 @@ pub const utils = struct { pub usingnamespace @import("utils/varint.zig"); }; +pub const rand = struct { + pub usingnamespace @import("rand/rand.zig"); + pub usingnamespace @import("rand/chacha.zig"); +}; + pub const trace = struct { pub usingnamespace @import("trace/level.zig"); pub usingnamespace @import("trace/log.zig"); diff --git a/src/utils/chacha.zig b/src/rand/chacha.zig similarity index 91% rename from src/utils/chacha.zig rename to src/rand/chacha.zig index ce3e857e8..8c4fee000 100644 --- a/src/utils/chacha.zig +++ b/src/rand/chacha.zig @@ -9,59 +9,17 @@ const sig = @import("../lib.zig"); const mem = std.mem; -const Return = sig.utils.Return; +const BlockRng = sig.utils.BlockRng; const BUFSZ: usize = 64; const endian = builtin.cpu.arch.endian(); /// A random number generator based on ChaCha. -/// Generates the same stream as `rand_chacha` +/// Generates the same stream as ChaChaRng in `rand_chacha` pub fn ChaChaRng(comptime rounds: usize) type { return BlockRng(ChaCha(rounds), ChaCha(rounds).generate); } -/// Wrapper for random number generators which generate -/// blocks of [64]u32. Minimizes calls to the underlying -/// random number generator by recycling unused data from -/// previous calls. Port of BlockRng from rust which -/// ensures the same sequence is generated. -pub fn BlockRng( - comptime T: type, - comptime generate: fn (*T, *[64]u32) void, -) type { - return struct { - results: [64]u32 = undefined, - index: usize = 64, - core: T, - - const Self = @This(); - - pub fn init(seed: anytype) Self { - return .{ .core = @call(.auto, T.init, .{seed}) }; - } - - pub fn random(self: *Self) std.rand.Random { - return std.rand.Random.init(self, fill); - } - - pub fn fill(self: *Self, dest: []u8) void { - var completed_bytes: usize = 0; - while (completed_bytes < dest.len) { - if (self.index >= self.results.len) { - generate(&self.core, &self.results); - self.index = 0; - } - const src: [*]u8 = @ptrCast(self.results[self.index..].ptr); - const num_u8s = @min(4 * (64 - self.index), dest.len - completed_bytes); - @memcpy(dest[completed_bytes..][0..num_u8s], src[0..num_u8s]); - - self.index += (num_u8s + 3) / 4; - completed_bytes += num_u8s; - } - } - }; -} - /// Computes the chacha stream. /// /// This is the underlying implementation of the chacha cipher. @@ -98,14 +56,14 @@ pub fn ChaCha(rounds: usize) type { .a = .{ k, k, k, k }, .b = .{ b, b, b, b }, .c = .{ c, c, c, c }, - .d = repeatx4AndAdd0123(self.d), + .d = repeat4timesAndAdd0123(self.d), }; for (0..rounds / 2) |_| { x = diagonalize(round(diagonalize(round(x), 1)), -1); } const sb = self.b; const sc = self.c; - const sd = repeatx4AndAdd0123(self.d); + const sd = repeat4timesAndAdd0123(self.d); const results: [64]u32 = @bitCast(transpose4(.{ wrappingAddEachInt(x.a, .{ k, k, k, k }), wrappingAddEachInt(x.b, .{ sb, sb, sb, sb }), @@ -156,7 +114,8 @@ fn wrappingAddToFirstHalf(d: [4]u32, i: u64) [4]u32 { return leIntBitCast([4]u32, u64s); } -fn repeatx4AndAdd0123(d: [4]u32) [4][4]u32 { + +fn repeat4timesAndAdd0123(d: [4]u32) [4][4]u32 { return .{ wrappingAddToFirstHalf(d, 0), wrappingAddToFirstHalf(d, 1), @@ -409,7 +368,7 @@ test "d0123 works" { .{ 3, 2, 3, 4 }, .{ 4, 2, 3, 4 }, }; - const output = repeatx4AndAdd0123(input); + const output = repeat4timesAndAdd0123(input); for (0..4) |i| { try std.testing.expect(mem.eql(u32, &expected_out[i], &output[i])); } diff --git a/src/rand/rand.zig b/src/rand/rand.zig new file mode 100644 index 000000000..8e1d9e599 --- /dev/null +++ b/src/rand/rand.zig @@ -0,0 +1,151 @@ +const std = @import("std"); +const sig = @import("../lib.zig"); + +const Allocator = std.mem.Allocator; +const Random = std.Random; + +const ChaChaRng = sig.rand.ChaCha; + +/// Uniformly samples a collection of weighted items. +/// This struct only deals with the weights, and it +/// tells you which index it selects. +/// +/// Each index's probability of being selected is the +/// ratio of its weight to the sum of all weights. +/// +/// For example, for the weights [1, 3, 2], the +/// probability of `sample` returning each index is: +/// 0 -> 1/6 +/// 1 -> 1/2 +/// 3 -> 1/3 +pub fn WeightedRandomSampler(comptime uint: type) type { + return struct { + allocator: Allocator, + random: Random, + cumulative_weights: []const uint, + total: uint, + + const Self = @This(); + + fn init( + allocator: Allocator, + random: Random, + weights: []const uint, + ) Allocator.Error!Self { + var cumulative_weights: []uint = try allocator.alloc(uint, weights.len); + var total: uint = 0; + for (0..weights.len) |i| { + total += weights[i]; + cumulative_weights[i] = total; + } + return .{ + .allocator = allocator, + .random = random, + .cumulative_weights = cumulative_weights, + .total = total, + }; + } + + fn deinit(self: Self) void { + self.allocator.free(self.cumulative_weights); + } + + /// Returns the index of the selected item + fn sample(self: *const Self) uint { + const want = self.random.uintLessThan(uint, self.total + 1); + var lower: usize = 0; + var upper: usize = self.cumulative_weights.len - 1; + var guess: usize = self.cumulative_weights.len * want / self.total; + for (0..self.cumulative_weights.len) |_| { + if (self.cumulative_weights[guess] >= want) { + upper = guess; + } else { + lower = guess + 1; + } + if (upper == lower) { + return upper; + } + guess = lower + (upper - lower) / 2; + } + unreachable; + } + }; +} + +/// Wrapper for random number generators which generate +/// blocks of [64]u32. Minimizes calls to the underlying +/// random number generator by recycling unused data from +/// previous calls. Port of BlockRng from rust which +/// ensures the same sequence is generated. +pub fn BlockRng( + comptime T: type, + comptime generate: fn (*T, *[64]u32) void, +) type { + return struct { + results: [64]u32 = undefined, + index: usize = 64, + core: T, + + const Self = @This(); + + pub fn init(seed: anytype) Self { + return .{ .core = @call(.auto, T.init, .{seed}) }; + } + + pub fn random(self: *Self) Random { + return Random.init(self, fill); + } + + pub fn fill(self: *Self, dest: []u8) void { + var completed_bytes: usize = 0; + while (completed_bytes < dest.len) { + if (self.index >= self.results.len) { + generate(&self.core, &self.results); + self.index = 0; + } + const src: [*]u8 = @ptrCast(self.results[self.index..].ptr); + const num_u8s = @min(4 * (64 - self.index), dest.len - completed_bytes); + @memcpy(dest[completed_bytes..][0..num_u8s], src[0..num_u8s]); + + self.index += (num_u8s + 3) / 4; + completed_bytes += num_u8s; + } + } + }; +} + +test "WeightedRandomSampler matches rust with chacha" { + // generate data + var rng = ChaChaRng(20).init(.{0} ** 32); + var random = rng.random(); + var items: [100]u64 = undefined; + for (0..100) |i| { + items[i] = @intCast(random.int(u32)); + } + + // run test + const idx = try WeightedRandomSampler(u64).init(std.testing.allocator, random, &items); + defer idx.deinit(); + for (0..100) |i| { + const choice = items[idx.sample()]; + try std.testing.expect(expected_weights[i] == choice); + } +} + +const expected_weights = [_]u64{ + 2956161493, 1129244316, 3088700093, 3781961315, 3373288848, 3202811807, 3373288848, + 3848953152, 2448479257, 3848953152, 772637944, 3781961315, 2813985970, 3612365086, + 1651635039, 2419978656, 1300932346, 3678279626, 683509331, 3612365086, 2086224346, + 3678279626, 3328365435, 3230977993, 2115397425, 3478228973, 2687045579, 3438229160, + 1973446681, 3373288848, 2419978656, 4248444456, 1867348299, 4064846400, 3678279626, + 4064846400, 3373288848, 3373288848, 2240211114, 3678279626, 1300932346, 2254827186, + 3848953152, 1867348299, 1194017814, 2254827186, 3373288848, 1651635039, 3328365435, + 3202811807, 3848953152, 2370328401, 3230977993, 2050511189, 2917185654, 3612365086, + 2576249230, 3438229160, 2866421973, 3438229160, 3612365086, 1669812906, 1768285000, + 877052848, 3755235835, 1651635039, 1931970043, 2813985970, 3781961315, 1004543717, + 2702218887, 2419978656, 2576249230, 2229903491, 4248444456, 3984256562, 4248444456, + 3339548555, 2576249230, 3848953152, 1071654007, 4064846400, 772637944, 4248444456, + 2448479257, 2229903491, 4294454303, 2813985970, 2971532662, 147947182, 2370328401, + 1981921065, 3478228973, 1387042214, 3755235835, 3384151174, 2448479257, 1768285000, + 102030521, 1813932776, +}; From 0322760f4e363d9bfb101cb64e856078a5aaf5de Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Thu, 23 May 2024 10:48:09 -0400 Subject: [PATCH 03/28] feat(core): leader schedule calculator --- src/accountsdb/genesis_config.zig | 8 + src/core/leader_schedule.zig | 256 ++++++++++++++++++++++++++++++ src/lib.zig | 2 +- src/rand/chacha.zig | 105 +++++++----- src/rand/rand.zig | 50 +++--- 5 files changed, 353 insertions(+), 68 deletions(-) create mode 100644 src/core/leader_schedule.zig diff --git a/src/accountsdb/genesis_config.zig b/src/accountsdb/genesis_config.zig index ac6cafbcd..e645c45b7 100644 --- a/src/accountsdb/genesis_config.zig +++ b/src/accountsdb/genesis_config.zig @@ -141,6 +141,14 @@ pub const EpochSchedule = extern struct { }; } } + + /// get the length of the given epoch (in slots) + pub fn getSlotsInEpoch(self: *const EpochSchedule, epoch: Epoch) Slot { + return if (epoch < self.first_normal_epoch) + 1 <<| epoch +| @ctz(MINIMUM_SLOTS_PER_EPOCH) + else + self.slots_per_epoch; + } }; pub const ClusterType = enum(u8) { diff --git a/src/core/leader_schedule.zig b/src/core/leader_schedule.zig new file mode 100644 index 000000000..1a160ae1f --- /dev/null +++ b/src/core/leader_schedule.zig @@ -0,0 +1,256 @@ +const std = @import("std"); +const sig = @import("../lib.zig"); + +const Allocator = std.mem.Allocator; + +const Bank = sig.accounts_db.Bank; +const ChaChaRng = sig.rand.ChaChaRng; +const Epoch = sig.core.Epoch; +const EpochStakes = sig.accounts_db.EpochStakes; +const Pubkey = sig.core.Pubkey; +const Slot = sig.core.Slot; +const WeightedRandomSampler = sig.rand.WeightedRandomSampler; + +pub const NUM_CONSECUTIVE_LEADER_SLOTS: u64 = 4; + +pub fn leaderScheduleFromBank(allocator: Allocator, bank: *const Bank, epoch: Epoch) Allocator.Error!?[]Pubkey { + const epoch_stakes = bank.bank_fields.epoch_stakes.getPtr(epoch) orelse return null; + const staked_nodes = &epoch_stakes.stakes.vote_accounts.staked_nodes orelse return null; + const slots_in_epoch = bank.bank_fields.epoch_schedule.getSlotsInEpoch(epoch); + leaderSchedule(allocator, staked_nodes, slots_in_epoch); +} + +pub fn leaderSchedule( + allocator: Allocator, + staked_nodes: *const std.AutoHashMap(Pubkey, u64), + slots_in_epoch: Slot, + epoch: Epoch, +) Allocator.Error![]Pubkey { + const Entry = std.AutoHashMap(Pubkey, u64).Entry; + + // sort nodes descending by stake weight, then pubkey + const nodes = try allocator.alloc(Entry, staked_nodes.count()); + defer allocator.free(nodes); + var iter = staked_nodes.iterator(); + var index: usize = 0; + while (iter.next()) |staked_node_entry| : (index += 1) { + nodes[index] = staked_node_entry; + } + std.mem.sortUnstable(Entry, nodes, {}, struct { + fn gt(_: void, lhs: Entry, rhs: Entry) bool { + return switch (std.math.order(lhs.value_ptr.*, rhs.value_ptr.*)) { + .gt => true, + .lt => false, + .eq => .gt == std.mem.order(u8, &lhs.key_ptr.data, &rhs.key_ptr.data), + }; + } + }.gt); + + // init random number generator + var seed: [32]u8 = .{0} ** 32; + std.mem.writeInt(Epoch, seed[0..@sizeOf(Epoch)], epoch, .little); + var rng = ChaChaRng(20).fromSeed(seed); + const random = rng.random(); + + // init sampler from stake weights + const stakes = try allocator.alloc(u64, nodes.len); + defer allocator.free(stakes); + for (nodes, 0..) |entry, i| stakes[i] = entry.value_ptr.*; + var sampler = try WeightedRandomSampler(u64).init(allocator, random, stakes); + defer sampler.deinit(); + + // calculate leader schedule + const slot_leaders = try allocator.alloc(Pubkey, slots_in_epoch); + var current_node: Pubkey = undefined; + for (0..slots_in_epoch) |i| { + if (i % NUM_CONSECUTIVE_LEADER_SLOTS == 0) { + current_node = nodes[sampler.sample()].key_ptr.*; + } + slot_leaders[i] = current_node; + } + + return slot_leaders; +} + +test "leaderSchedule calculation matches agave" { + var rng = ChaChaRng(20).fromSeed(.{0} ** 32); + const random = rng.random(); + var pubkey_bytes: [32]u8 = undefined; + var staked_nodes = std.AutoHashMap(Pubkey, u64).init(std.testing.allocator); + defer staked_nodes.deinit(); + for (0..100) |_| { + random.bytes(&pubkey_bytes); + const key = Pubkey{ .data = pubkey_bytes }; + const stake = random.int(u64) / 1000; + try staked_nodes.put(key, stake); + } + const slot_leaders = try leaderSchedule(std.testing.allocator, &staked_nodes, 321, 123); + defer std.testing.allocator.free(slot_leaders); + for (slot_leaders, 0..) |slot_leader, i| { + try std.testing.expect((try Pubkey.fromString(expected[i])).equals(&slot_leader)); + } +} + +const expected = [_][]const u8{ + "HU1g6zZ3LrXJeFYEmDAekv44kAv9XE8g8FQYz3rSrmNY", "HU1g6zZ3LrXJeFYEmDAekv44kAv9XE8g8FQYz3rSrmNY", + "HU1g6zZ3LrXJeFYEmDAekv44kAv9XE8g8FQYz3rSrmNY", "HU1g6zZ3LrXJeFYEmDAekv44kAv9XE8g8FQYz3rSrmNY", + "AvsmCG8R1qGJtRvjqudkX974ihfbYZUVf4t515tzxyHv", "AvsmCG8R1qGJtRvjqudkX974ihfbYZUVf4t515tzxyHv", + "AvsmCG8R1qGJtRvjqudkX974ihfbYZUVf4t515tzxyHv", "AvsmCG8R1qGJtRvjqudkX974ihfbYZUVf4t515tzxyHv", + "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", + "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", + "CrFNvAe9JJkW9yzmtUMWpA4GMvRqWz88EJLPyhrxmFzd", "CrFNvAe9JJkW9yzmtUMWpA4GMvRqWz88EJLPyhrxmFzd", + "CrFNvAe9JJkW9yzmtUMWpA4GMvRqWz88EJLPyhrxmFzd", "CrFNvAe9JJkW9yzmtUMWpA4GMvRqWz88EJLPyhrxmFzd", + "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", + "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", + "ChZKtGvACLPovxKJLUtnDyNHiPLECoXsneziARENU8kV", "ChZKtGvACLPovxKJLUtnDyNHiPLECoXsneziARENU8kV", + "ChZKtGvACLPovxKJLUtnDyNHiPLECoXsneziARENU8kV", "ChZKtGvACLPovxKJLUtnDyNHiPLECoXsneziARENU8kV", + "4SNVzDbWzQmUWRVb5BNXifV4NYGoYA8evhpSFfy5pSaN", "4SNVzDbWzQmUWRVb5BNXifV4NYGoYA8evhpSFfy5pSaN", + "4SNVzDbWzQmUWRVb5BNXifV4NYGoYA8evhpSFfy5pSaN", "4SNVzDbWzQmUWRVb5BNXifV4NYGoYA8evhpSFfy5pSaN", + "5nvNxUpHfZ2FSRPbDtDyMeFrvN5YBFBvokoYobe2qgqH", "5nvNxUpHfZ2FSRPbDtDyMeFrvN5YBFBvokoYobe2qgqH", + "5nvNxUpHfZ2FSRPbDtDyMeFrvN5YBFBvokoYobe2qgqH", "5nvNxUpHfZ2FSRPbDtDyMeFrvN5YBFBvokoYobe2qgqH", + "8zScg5nWKZEzFJnhPu5s9zBeLRHTTjvfcw2aKWDRJNDt", "8zScg5nWKZEzFJnhPu5s9zBeLRHTTjvfcw2aKWDRJNDt", + "8zScg5nWKZEzFJnhPu5s9zBeLRHTTjvfcw2aKWDRJNDt", "8zScg5nWKZEzFJnhPu5s9zBeLRHTTjvfcw2aKWDRJNDt", + "ALiW6m6KxrY98DkUhToCU8eLfgrQ73Zuo3eh9phWmbJV", "ALiW6m6KxrY98DkUhToCU8eLfgrQ73Zuo3eh9phWmbJV", + "ALiW6m6KxrY98DkUhToCU8eLfgrQ73Zuo3eh9phWmbJV", "ALiW6m6KxrY98DkUhToCU8eLfgrQ73Zuo3eh9phWmbJV", + "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", + "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", + "8LetEQHxeoVTMFoQiett6odLZTwKw1SLYnC6UiNHaNC9", "8LetEQHxeoVTMFoQiett6odLZTwKw1SLYnC6UiNHaNC9", + "8LetEQHxeoVTMFoQiett6odLZTwKw1SLYnC6UiNHaNC9", "8LetEQHxeoVTMFoQiett6odLZTwKw1SLYnC6UiNHaNC9", + "Gh8qe5sKntpd7RMhLSy52CZcwEZZVrPujNxFc6FCsXSC", "Gh8qe5sKntpd7RMhLSy52CZcwEZZVrPujNxFc6FCsXSC", + "Gh8qe5sKntpd7RMhLSy52CZcwEZZVrPujNxFc6FCsXSC", "Gh8qe5sKntpd7RMhLSy52CZcwEZZVrPujNxFc6FCsXSC", + "F8RUGg4CfVvsHGH38aAR3s2nsw7Faw2QjhsWomScgEtb", "F8RUGg4CfVvsHGH38aAR3s2nsw7Faw2QjhsWomScgEtb", + "F8RUGg4CfVvsHGH38aAR3s2nsw7Faw2QjhsWomScgEtb", "F8RUGg4CfVvsHGH38aAR3s2nsw7Faw2QjhsWomScgEtb", + "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", + "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", + "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", + "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", + "6BSbQZxg86LrAtscs3cezszNJmJhRWErG72VWavECEz6", "6BSbQZxg86LrAtscs3cezszNJmJhRWErG72VWavECEz6", + "6BSbQZxg86LrAtscs3cezszNJmJhRWErG72VWavECEz6", "6BSbQZxg86LrAtscs3cezszNJmJhRWErG72VWavECEz6", + "ALiW6m6KxrY98DkUhToCU8eLfgrQ73Zuo3eh9phWmbJV", "ALiW6m6KxrY98DkUhToCU8eLfgrQ73Zuo3eh9phWmbJV", + "ALiW6m6KxrY98DkUhToCU8eLfgrQ73Zuo3eh9phWmbJV", "ALiW6m6KxrY98DkUhToCU8eLfgrQ73Zuo3eh9phWmbJV", + "GmNETdrkoh2trUWJ4bHP6TLsHw2jcbdwTDyKYToYNTqU", "GmNETdrkoh2trUWJ4bHP6TLsHw2jcbdwTDyKYToYNTqU", + "GmNETdrkoh2trUWJ4bHP6TLsHw2jcbdwTDyKYToYNTqU", "GmNETdrkoh2trUWJ4bHP6TLsHw2jcbdwTDyKYToYNTqU", + "GGZKvA54JkUQ66NqkLAeo7uqu9dJdneXm9gsPqTbNEMY", "GGZKvA54JkUQ66NqkLAeo7uqu9dJdneXm9gsPqTbNEMY", + "GGZKvA54JkUQ66NqkLAeo7uqu9dJdneXm9gsPqTbNEMY", "GGZKvA54JkUQ66NqkLAeo7uqu9dJdneXm9gsPqTbNEMY", + "Ar17KaAgMEiGVVRQqo3ta7jfeAksR3JcXaRsiDSBxgz3", "Ar17KaAgMEiGVVRQqo3ta7jfeAksR3JcXaRsiDSBxgz3", + "Ar17KaAgMEiGVVRQqo3ta7jfeAksR3JcXaRsiDSBxgz3", "Ar17KaAgMEiGVVRQqo3ta7jfeAksR3JcXaRsiDSBxgz3", + "EgYg66jU5q678BdPGEPj1fyobsPXzwLoxz2uvvSUQ2zG", "EgYg66jU5q678BdPGEPj1fyobsPXzwLoxz2uvvSUQ2zG", + "EgYg66jU5q678BdPGEPj1fyobsPXzwLoxz2uvvSUQ2zG", "EgYg66jU5q678BdPGEPj1fyobsPXzwLoxz2uvvSUQ2zG", + "EtnzJyeepGFXSJZ7EWqi1kXYi2zpgFMdUtDx1ovRTi75", "EtnzJyeepGFXSJZ7EWqi1kXYi2zpgFMdUtDx1ovRTi75", + "EtnzJyeepGFXSJZ7EWqi1kXYi2zpgFMdUtDx1ovRTi75", "EtnzJyeepGFXSJZ7EWqi1kXYi2zpgFMdUtDx1ovRTi75", + "DCKCfRPPfHUHmJz7ejnLCQodkhsuKj51exdXUBWEivQB", "DCKCfRPPfHUHmJz7ejnLCQodkhsuKj51exdXUBWEivQB", + "DCKCfRPPfHUHmJz7ejnLCQodkhsuKj51exdXUBWEivQB", "DCKCfRPPfHUHmJz7ejnLCQodkhsuKj51exdXUBWEivQB", + "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", + "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", + "6BSbQZxg86LrAtscs3cezszNJmJhRWErG72VWavECEz6", "6BSbQZxg86LrAtscs3cezszNJmJhRWErG72VWavECEz6", + "6BSbQZxg86LrAtscs3cezszNJmJhRWErG72VWavECEz6", "6BSbQZxg86LrAtscs3cezszNJmJhRWErG72VWavECEz6", + "DxMVzBzTuX2VprSQEvtKraPR5JVMSgx4rqyASG4xEVNW", "DxMVzBzTuX2VprSQEvtKraPR5JVMSgx4rqyASG4xEVNW", + "DxMVzBzTuX2VprSQEvtKraPR5JVMSgx4rqyASG4xEVNW", "DxMVzBzTuX2VprSQEvtKraPR5JVMSgx4rqyASG4xEVNW", + "3dMU1xcDSXzaG9mFB8N6ySKsSE1AknaxPAYBxCs83qsn", "3dMU1xcDSXzaG9mFB8N6ySKsSE1AknaxPAYBxCs83qsn", + "3dMU1xcDSXzaG9mFB8N6ySKsSE1AknaxPAYBxCs83qsn", "3dMU1xcDSXzaG9mFB8N6ySKsSE1AknaxPAYBxCs83qsn", + "36hQwDVUzUBqij3vukdrjGogxjH1qzve66vMLLHgkoNG", "36hQwDVUzUBqij3vukdrjGogxjH1qzve66vMLLHgkoNG", + "36hQwDVUzUBqij3vukdrjGogxjH1qzve66vMLLHgkoNG", "36hQwDVUzUBqij3vukdrjGogxjH1qzve66vMLLHgkoNG", + "8GfdNsue2yP6dagvMwK9YKWKhxteELz1JGeMdy7b3Xtp", "8GfdNsue2yP6dagvMwK9YKWKhxteELz1JGeMdy7b3Xtp", + "8GfdNsue2yP6dagvMwK9YKWKhxteELz1JGeMdy7b3Xtp", "8GfdNsue2yP6dagvMwK9YKWKhxteELz1JGeMdy7b3Xtp", + "A5TCWz8baPdYYgeCa5scXwNEUmnsYwbWbmGLbUSYergs", "A5TCWz8baPdYYgeCa5scXwNEUmnsYwbWbmGLbUSYergs", + "A5TCWz8baPdYYgeCa5scXwNEUmnsYwbWbmGLbUSYergs", "A5TCWz8baPdYYgeCa5scXwNEUmnsYwbWbmGLbUSYergs", + "EiLPxcwYe8akU9g6C6A99j5mep3N7A4ySfMNunC3qMjQ", "EiLPxcwYe8akU9g6C6A99j5mep3N7A4ySfMNunC3qMjQ", + "EiLPxcwYe8akU9g6C6A99j5mep3N7A4ySfMNunC3qMjQ", "EiLPxcwYe8akU9g6C6A99j5mep3N7A4ySfMNunC3qMjQ", + "4Lu8CGxdYgXAHKUMU3bN4BnDSYdcqniNV4WFJ2GiY5wz", "4Lu8CGxdYgXAHKUMU3bN4BnDSYdcqniNV4WFJ2GiY5wz", + "4Lu8CGxdYgXAHKUMU3bN4BnDSYdcqniNV4WFJ2GiY5wz", "4Lu8CGxdYgXAHKUMU3bN4BnDSYdcqniNV4WFJ2GiY5wz", + "9BF6Dt4ELaWvZ88sdKkwx6LPvo51w7A3FG5dqTFBnNC6", "9BF6Dt4ELaWvZ88sdKkwx6LPvo51w7A3FG5dqTFBnNC6", + "9BF6Dt4ELaWvZ88sdKkwx6LPvo51w7A3FG5dqTFBnNC6", "9BF6Dt4ELaWvZ88sdKkwx6LPvo51w7A3FG5dqTFBnNC6", + "8GfdNsue2yP6dagvMwK9YKWKhxteELz1JGeMdy7b3Xtp", "8GfdNsue2yP6dagvMwK9YKWKhxteELz1JGeMdy7b3Xtp", + "8GfdNsue2yP6dagvMwK9YKWKhxteELz1JGeMdy7b3Xtp", "8GfdNsue2yP6dagvMwK9YKWKhxteELz1JGeMdy7b3Xtp", + "ChZKtGvACLPovxKJLUtnDyNHiPLECoXsneziARENU8kV", "ChZKtGvACLPovxKJLUtnDyNHiPLECoXsneziARENU8kV", + "ChZKtGvACLPovxKJLUtnDyNHiPLECoXsneziARENU8kV", "ChZKtGvACLPovxKJLUtnDyNHiPLECoXsneziARENU8kV", + "DCKCfRPPfHUHmJz7ejnLCQodkhsuKj51exdXUBWEivQB", "DCKCfRPPfHUHmJz7ejnLCQodkhsuKj51exdXUBWEivQB", + "DCKCfRPPfHUHmJz7ejnLCQodkhsuKj51exdXUBWEivQB", "DCKCfRPPfHUHmJz7ejnLCQodkhsuKj51exdXUBWEivQB", + "HzAQzrCnH7VAaxAVSahEs6WcBcW38bi3ZLarZaZ1YVR4", "HzAQzrCnH7VAaxAVSahEs6WcBcW38bi3ZLarZaZ1YVR4", + "HzAQzrCnH7VAaxAVSahEs6WcBcW38bi3ZLarZaZ1YVR4", "HzAQzrCnH7VAaxAVSahEs6WcBcW38bi3ZLarZaZ1YVR4", + "4XVPmBXM6bfJdUqkLfAxS6t4fsZS9D3rSZd3M835u91H", "4XVPmBXM6bfJdUqkLfAxS6t4fsZS9D3rSZd3M835u91H", + "4XVPmBXM6bfJdUqkLfAxS6t4fsZS9D3rSZd3M835u91H", "4XVPmBXM6bfJdUqkLfAxS6t4fsZS9D3rSZd3M835u91H", + "HzAQzrCnH7VAaxAVSahEs6WcBcW38bi3ZLarZaZ1YVR4", "HzAQzrCnH7VAaxAVSahEs6WcBcW38bi3ZLarZaZ1YVR4", + "HzAQzrCnH7VAaxAVSahEs6WcBcW38bi3ZLarZaZ1YVR4", "HzAQzrCnH7VAaxAVSahEs6WcBcW38bi3ZLarZaZ1YVR4", + "8NaFkAtLW8qo4VdgUuU1VAD3nuKZxpDFMGpeg1ajeCSZ", "8NaFkAtLW8qo4VdgUuU1VAD3nuKZxpDFMGpeg1ajeCSZ", + "8NaFkAtLW8qo4VdgUuU1VAD3nuKZxpDFMGpeg1ajeCSZ", "8NaFkAtLW8qo4VdgUuU1VAD3nuKZxpDFMGpeg1ajeCSZ", + "J8pKv47cms17Qav9s97pJjKhQLRvmQbxGMLKVe7QXF7P", "J8pKv47cms17Qav9s97pJjKhQLRvmQbxGMLKVe7QXF7P", + "J8pKv47cms17Qav9s97pJjKhQLRvmQbxGMLKVe7QXF7P", "J8pKv47cms17Qav9s97pJjKhQLRvmQbxGMLKVe7QXF7P", + "A5TCWz8baPdYYgeCa5scXwNEUmnsYwbWbmGLbUSYergs", "A5TCWz8baPdYYgeCa5scXwNEUmnsYwbWbmGLbUSYergs", + "A5TCWz8baPdYYgeCa5scXwNEUmnsYwbWbmGLbUSYergs", "A5TCWz8baPdYYgeCa5scXwNEUmnsYwbWbmGLbUSYergs", + "ErAWjNHKa2oChJcqdyoCXC5ZZsLqpGwphcbmMxTEwmsZ", "ErAWjNHKa2oChJcqdyoCXC5ZZsLqpGwphcbmMxTEwmsZ", + "ErAWjNHKa2oChJcqdyoCXC5ZZsLqpGwphcbmMxTEwmsZ", "ErAWjNHKa2oChJcqdyoCXC5ZZsLqpGwphcbmMxTEwmsZ", + "8NaFkAtLW8qo4VdgUuU1VAD3nuKZxpDFMGpeg1ajeCSZ", "8NaFkAtLW8qo4VdgUuU1VAD3nuKZxpDFMGpeg1ajeCSZ", + "8NaFkAtLW8qo4VdgUuU1VAD3nuKZxpDFMGpeg1ajeCSZ", "8NaFkAtLW8qo4VdgUuU1VAD3nuKZxpDFMGpeg1ajeCSZ", + "78DHaJZHsmTj5g6xPQJa5pP99Tg6MgG8zQVqcKG5Zq7x", "78DHaJZHsmTj5g6xPQJa5pP99Tg6MgG8zQVqcKG5Zq7x", + "78DHaJZHsmTj5g6xPQJa5pP99Tg6MgG8zQVqcKG5Zq7x", "78DHaJZHsmTj5g6xPQJa5pP99Tg6MgG8zQVqcKG5Zq7x", + "DWUt7KxRWF8GdFhTYdM4ZFA3rQ9roKrRXUcm8Xeeywkq", "DWUt7KxRWF8GdFhTYdM4ZFA3rQ9roKrRXUcm8Xeeywkq", + "DWUt7KxRWF8GdFhTYdM4ZFA3rQ9roKrRXUcm8Xeeywkq", "DWUt7KxRWF8GdFhTYdM4ZFA3rQ9roKrRXUcm8Xeeywkq", + "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", + "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", + "Esqc3WZPLR1XkvapZxRqFTQa6UxGQM4Lamqt6duLFEtj", "Esqc3WZPLR1XkvapZxRqFTQa6UxGQM4Lamqt6duLFEtj", + "Esqc3WZPLR1XkvapZxRqFTQa6UxGQM4Lamqt6duLFEtj", "Esqc3WZPLR1XkvapZxRqFTQa6UxGQM4Lamqt6duLFEtj", + "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", + "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", + "8NaFkAtLW8qo4VdgUuU1VAD3nuKZxpDFMGpeg1ajeCSZ", "8NaFkAtLW8qo4VdgUuU1VAD3nuKZxpDFMGpeg1ajeCSZ", + "8NaFkAtLW8qo4VdgUuU1VAD3nuKZxpDFMGpeg1ajeCSZ", "8NaFkAtLW8qo4VdgUuU1VAD3nuKZxpDFMGpeg1ajeCSZ", + "8NaFkAtLW8qo4VdgUuU1VAD3nuKZxpDFMGpeg1ajeCSZ", "8NaFkAtLW8qo4VdgUuU1VAD3nuKZxpDFMGpeg1ajeCSZ", + "8NaFkAtLW8qo4VdgUuU1VAD3nuKZxpDFMGpeg1ajeCSZ", "8NaFkAtLW8qo4VdgUuU1VAD3nuKZxpDFMGpeg1ajeCSZ", + "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", + "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", "5W6GpY2dKVsks2QF1EdrDSasdM1f9KqVNVFbZTzBy8V", + "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", + "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", "Ay73RcvjzYq43dTv32CzTEhddBBQJL6J5JnzbJjTFQZN", + "DCKCfRPPfHUHmJz7ejnLCQodkhsuKj51exdXUBWEivQB", "DCKCfRPPfHUHmJz7ejnLCQodkhsuKj51exdXUBWEivQB", + "DCKCfRPPfHUHmJz7ejnLCQodkhsuKj51exdXUBWEivQB", "DCKCfRPPfHUHmJz7ejnLCQodkhsuKj51exdXUBWEivQB", + "ErAWjNHKa2oChJcqdyoCXC5ZZsLqpGwphcbmMxTEwmsZ", "ErAWjNHKa2oChJcqdyoCXC5ZZsLqpGwphcbmMxTEwmsZ", + "ErAWjNHKa2oChJcqdyoCXC5ZZsLqpGwphcbmMxTEwmsZ", "ErAWjNHKa2oChJcqdyoCXC5ZZsLqpGwphcbmMxTEwmsZ", + "9CsaB86comVhyFqtDrALpBHhaBHGmf13iBJL7JDWV9p2", "9CsaB86comVhyFqtDrALpBHhaBHGmf13iBJL7JDWV9p2", + "9CsaB86comVhyFqtDrALpBHhaBHGmf13iBJL7JDWV9p2", "9CsaB86comVhyFqtDrALpBHhaBHGmf13iBJL7JDWV9p2", + "DxMVzBzTuX2VprSQEvtKraPR5JVMSgx4rqyASG4xEVNW", "DxMVzBzTuX2VprSQEvtKraPR5JVMSgx4rqyASG4xEVNW", + "DxMVzBzTuX2VprSQEvtKraPR5JVMSgx4rqyASG4xEVNW", "DxMVzBzTuX2VprSQEvtKraPR5JVMSgx4rqyASG4xEVNW", + "FLG8C3rziE56N3jib3NPB1TcTrGJXeTmLLwvPCW337PA", "FLG8C3rziE56N3jib3NPB1TcTrGJXeTmLLwvPCW337PA", + "FLG8C3rziE56N3jib3NPB1TcTrGJXeTmLLwvPCW337PA", "FLG8C3rziE56N3jib3NPB1TcTrGJXeTmLLwvPCW337PA", + "6b4LnC2vhdfS5MqjBSwWMFdJwA9tgbu2ezEcspeKVYSn", "6b4LnC2vhdfS5MqjBSwWMFdJwA9tgbu2ezEcspeKVYSn", + "6b4LnC2vhdfS5MqjBSwWMFdJwA9tgbu2ezEcspeKVYSn", "6b4LnC2vhdfS5MqjBSwWMFdJwA9tgbu2ezEcspeKVYSn", + "6BSbQZxg86LrAtscs3cezszNJmJhRWErG72VWavECEz6", "6BSbQZxg86LrAtscs3cezszNJmJhRWErG72VWavECEz6", + "6BSbQZxg86LrAtscs3cezszNJmJhRWErG72VWavECEz6", "6BSbQZxg86LrAtscs3cezszNJmJhRWErG72VWavECEz6", + "8gxPGDZK4G8qzW7zsRpw8MW84rRpeUS6vj8CGrPbYdyk", "8gxPGDZK4G8qzW7zsRpw8MW84rRpeUS6vj8CGrPbYdyk", + "8gxPGDZK4G8qzW7zsRpw8MW84rRpeUS6vj8CGrPbYdyk", "8gxPGDZK4G8qzW7zsRpw8MW84rRpeUS6vj8CGrPbYdyk", + "Hq6Tke5EnrpDADM4sTcfMZoSLwsNCNz4pmHpCoc4UdY9", "Hq6Tke5EnrpDADM4sTcfMZoSLwsNCNz4pmHpCoc4UdY9", + "Hq6Tke5EnrpDADM4sTcfMZoSLwsNCNz4pmHpCoc4UdY9", "Hq6Tke5EnrpDADM4sTcfMZoSLwsNCNz4pmHpCoc4UdY9", + "GGZKvA54JkUQ66NqkLAeo7uqu9dJdneXm9gsPqTbNEMY", "GGZKvA54JkUQ66NqkLAeo7uqu9dJdneXm9gsPqTbNEMY", + "GGZKvA54JkUQ66NqkLAeo7uqu9dJdneXm9gsPqTbNEMY", "GGZKvA54JkUQ66NqkLAeo7uqu9dJdneXm9gsPqTbNEMY", + "GwckxXocVzxE8Ao1nWrW6QmBCLiy3k5DuLE2uEV2RHAq", "GwckxXocVzxE8Ao1nWrW6QmBCLiy3k5DuLE2uEV2RHAq", + "GwckxXocVzxE8Ao1nWrW6QmBCLiy3k5DuLE2uEV2RHAq", "GwckxXocVzxE8Ao1nWrW6QmBCLiy3k5DuLE2uEV2RHAq", + "Gkkp1TrPTWZLRE88HNZvRfUh2Hjpgti9g7AqQv88cf9", "Gkkp1TrPTWZLRE88HNZvRfUh2Hjpgti9g7AqQv88cf9", + "Gkkp1TrPTWZLRE88HNZvRfUh2Hjpgti9g7AqQv88cf9", "Gkkp1TrPTWZLRE88HNZvRfUh2Hjpgti9g7AqQv88cf9", + "HCbPW8qzM3feTpyYrA1HS5byK7PqBq4m1cvZRuQT6yb1", "HCbPW8qzM3feTpyYrA1HS5byK7PqBq4m1cvZRuQT6yb1", + "HCbPW8qzM3feTpyYrA1HS5byK7PqBq4m1cvZRuQT6yb1", "HCbPW8qzM3feTpyYrA1HS5byK7PqBq4m1cvZRuQT6yb1", + "3r3Tzsck7WPJbsbPM9yhC8fsBQtjFFBwrGX2ct394Bef", "3r3Tzsck7WPJbsbPM9yhC8fsBQtjFFBwrGX2ct394Bef", + "3r3Tzsck7WPJbsbPM9yhC8fsBQtjFFBwrGX2ct394Bef", "3r3Tzsck7WPJbsbPM9yhC8fsBQtjFFBwrGX2ct394Bef", + "EiLPxcwYe8akU9g6C6A99j5mep3N7A4ySfMNunC3qMjQ", "EiLPxcwYe8akU9g6C6A99j5mep3N7A4ySfMNunC3qMjQ", + "EiLPxcwYe8akU9g6C6A99j5mep3N7A4ySfMNunC3qMjQ", "EiLPxcwYe8akU9g6C6A99j5mep3N7A4ySfMNunC3qMjQ", + "DxMVzBzTuX2VprSQEvtKraPR5JVMSgx4rqyASG4xEVNW", "DxMVzBzTuX2VprSQEvtKraPR5JVMSgx4rqyASG4xEVNW", + "DxMVzBzTuX2VprSQEvtKraPR5JVMSgx4rqyASG4xEVNW", "DxMVzBzTuX2VprSQEvtKraPR5JVMSgx4rqyASG4xEVNW", + "78DHaJZHsmTj5g6xPQJa5pP99Tg6MgG8zQVqcKG5Zq7x", "78DHaJZHsmTj5g6xPQJa5pP99Tg6MgG8zQVqcKG5Zq7x", + "78DHaJZHsmTj5g6xPQJa5pP99Tg6MgG8zQVqcKG5Zq7x", "78DHaJZHsmTj5g6xPQJa5pP99Tg6MgG8zQVqcKG5Zq7x", + "FLG8C3rziE56N3jib3NPB1TcTrGJXeTmLLwvPCW337PA", "FLG8C3rziE56N3jib3NPB1TcTrGJXeTmLLwvPCW337PA", + "FLG8C3rziE56N3jib3NPB1TcTrGJXeTmLLwvPCW337PA", "FLG8C3rziE56N3jib3NPB1TcTrGJXeTmLLwvPCW337PA", + "6BSbQZxg86LrAtscs3cezszNJmJhRWErG72VWavECEz6", "6BSbQZxg86LrAtscs3cezszNJmJhRWErG72VWavECEz6", + "6BSbQZxg86LrAtscs3cezszNJmJhRWErG72VWavECEz6", "6BSbQZxg86LrAtscs3cezszNJmJhRWErG72VWavECEz6", + "9qBV9MtqqSSt4pt8XvX8URn2fLQqNPWzcYiBx8rcAgiX", "9qBV9MtqqSSt4pt8XvX8URn2fLQqNPWzcYiBx8rcAgiX", + "9qBV9MtqqSSt4pt8XvX8URn2fLQqNPWzcYiBx8rcAgiX", "9qBV9MtqqSSt4pt8XvX8URn2fLQqNPWzcYiBx8rcAgiX", + "78DHaJZHsmTj5g6xPQJa5pP99Tg6MgG8zQVqcKG5Zq7x", "78DHaJZHsmTj5g6xPQJa5pP99Tg6MgG8zQVqcKG5Zq7x", + "78DHaJZHsmTj5g6xPQJa5pP99Tg6MgG8zQVqcKG5Zq7x", "78DHaJZHsmTj5g6xPQJa5pP99Tg6MgG8zQVqcKG5Zq7x", + "Hq6Tke5EnrpDADM4sTcfMZoSLwsNCNz4pmHpCoc4UdY9", "Hq6Tke5EnrpDADM4sTcfMZoSLwsNCNz4pmHpCoc4UdY9", + "Hq6Tke5EnrpDADM4sTcfMZoSLwsNCNz4pmHpCoc4UdY9", "Hq6Tke5EnrpDADM4sTcfMZoSLwsNCNz4pmHpCoc4UdY9", + "GGZKvA54JkUQ66NqkLAeo7uqu9dJdneXm9gsPqTbNEMY", "GGZKvA54JkUQ66NqkLAeo7uqu9dJdneXm9gsPqTbNEMY", + "GGZKvA54JkUQ66NqkLAeo7uqu9dJdneXm9gsPqTbNEMY", "GGZKvA54JkUQ66NqkLAeo7uqu9dJdneXm9gsPqTbNEMY", + "EffB1PCz4fwqLGD9ko1bkRTFVHekedCTX83az91Rdbo2", "EffB1PCz4fwqLGD9ko1bkRTFVHekedCTX83az91Rdbo2", + "EffB1PCz4fwqLGD9ko1bkRTFVHekedCTX83az91Rdbo2", "EffB1PCz4fwqLGD9ko1bkRTFVHekedCTX83az91Rdbo2", + "AuDjtyKmix6vLHBsfouA82GQrmJ4JRWRPqCEcD54kkkH", "AuDjtyKmix6vLHBsfouA82GQrmJ4JRWRPqCEcD54kkkH", + "AuDjtyKmix6vLHBsfouA82GQrmJ4JRWRPqCEcD54kkkH", "AuDjtyKmix6vLHBsfouA82GQrmJ4JRWRPqCEcD54kkkH", + "HU1g6zZ3LrXJeFYEmDAekv44kAv9XE8g8FQYz3rSrmNY", "HU1g6zZ3LrXJeFYEmDAekv44kAv9XE8g8FQYz3rSrmNY", + "HU1g6zZ3LrXJeFYEmDAekv44kAv9XE8g8FQYz3rSrmNY", "HU1g6zZ3LrXJeFYEmDAekv44kAv9XE8g8FQYz3rSrmNY", + "DUUJtWATcHjMhNHGh9pPud3HGp4yrrZe1tE7ELHXUAB6", +}; diff --git a/src/lib.zig b/src/lib.zig index eee614eff..0342cd87f 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -15,6 +15,7 @@ pub const core = struct { pub usingnamespace @import("core/time.zig"); pub usingnamespace @import("core/hard_forks.zig"); pub usingnamespace @import("core/shred.zig"); + pub usingnamespace @import("core/leader_schedule.zig"); }; pub const accounts_db = struct { @@ -63,7 +64,6 @@ pub const sync = struct { pub const utils = struct { pub usingnamespace @import("utils/arraylist.zig"); pub usingnamespace @import("utils/bitflags.zig"); - pub usingnamespace @import("utils/chacha.zig"); pub usingnamespace @import("utils/lazy.zig"); pub usingnamespace @import("utils/math.zig"); pub usingnamespace @import("utils/shortvec.zig"); diff --git a/src/rand/chacha.zig b/src/rand/chacha.zig index 8c4fee000..698249036 100644 --- a/src/rand/chacha.zig +++ b/src/rand/chacha.zig @@ -1,7 +1,12 @@ //! Port of ChaCha from the `rand_chacha` crate. -//! Generates the same psuedorandom numbers as rand_chacha, -//! unlike Zig std's ChaCha. This is needed since rand_chacha -//! does not comply with the IETF standard. +//! +//! Generates the same psuedorandom numbers as rand_chacha, unlike Zig std's +//! ChaCha. +//! +//! This is needed since rand_chacha differs from the zig std's ChaCha in several +//! ways. One example is that it does not comply with the IETF standard, plus +//! there are other compatibility issues that require a different design from zig +//! std, like how it maintains state across iterations. const std = @import("std"); const builtin = @import("builtin"); @@ -9,23 +14,36 @@ const sig = @import("../lib.zig"); const mem = std.mem; -const BlockRng = sig.utils.BlockRng; +const BlockRng = sig.rand.BlockRng; -const BUFSZ: usize = 64; const endian = builtin.cpu.arch.endian(); /// A random number generator based on ChaCha. -/// Generates the same stream as ChaChaRng in `rand_chacha` +/// Generates the same stream as ChaChaRng in `rand_chacha`. +/// This is an ease-of-use wrapper for the type: +/// BlockRng(ChaCha(rounds), ChaCha(rounds).generate) pub fn ChaChaRng(comptime rounds: usize) type { - return BlockRng(ChaCha(rounds), ChaCha(rounds).generate); + return struct { + block_rng: BlockRng(ChaCha(rounds), ChaCha(rounds).generate), + + const Self = @This(); + + pub fn fromSeed(seed: [32]u8) Self { + return .{ .block_rng = .{ .core = ChaCha(rounds).init(seed, .{0} ** 12) } }; + } + + pub fn random(self: *Self) std.rand.Random { + return self.block_rng.random(); + } + }; } /// Computes the chacha stream. /// -/// This is the underlying implementation of the chacha cipher. -/// If you're looking for a random number generator, ChaChaRng -/// is a better candidate. -pub fn ChaCha(rounds: usize) type { +/// This is the barebones implementation of the chacha stream cipher. If you're +/// looking for a random number generator based on the chacha stream cipher, use +/// ChaChaRng. +pub fn ChaCha(comptime rounds: usize) type { return struct { b: [4]u32, c: [4]u32, @@ -33,11 +51,7 @@ pub fn ChaCha(rounds: usize) type { const Self = @This(); - pub fn init(seed: [32]u8) Self { - return Self.initWithNonce(seed, .{0} ** 12); - } - - pub fn initWithNonce(key: [32]u8, nonce: [12]u8) Self { + pub fn init(key: [32]u8, nonce: [12]u8) Self { const ctr_nonce = .{0} ++ leIntBitCast([3]u32, nonce); return .{ .b = leIntBitCast([4]u32, key[0..16].*), @@ -46,10 +60,10 @@ pub fn ChaCha(rounds: usize) type { }; } - /// Run the full chacha algorithm, generating the next block - /// of 64 32-bit integers. + /// Run the full chacha algorithm, generating the next block of 64 32-bit + /// integers. pub fn generate(self: *Self, out: *[64]u32) void { - const k = [4]u32{ 0x6170_7865, 0x3320_646e, 0x7962_2d32, 0x6b20_6574 }; + const k = comptime leIntBitCast([4]u32, @as([16]u8, "expand 32-byte k".*)); const b = self.b; const c = self.c; var x = State{ @@ -105,16 +119,14 @@ fn transpose4(a: [4][4][4]u32) [4][4][4]u32 { }; } -/// converts the first two items into a u64 and -/// then wrapping_adds the integer `i` to it, -/// then converts back to u32s. +/// converts the first two items into a u64 and then wrapping_adds the integer +/// `i` to it, then converts back to u32s. fn wrappingAddToFirstHalf(d: [4]u32, i: u64) [4]u32 { var u64s = leIntBitCast([2]u64, d); u64s[0] += i; return leIntBitCast([4]u32, u64s); } - fn repeat4timesAndAdd0123(d: [4]u32) [4][4]u32 { return .{ wrappingAddToFirstHalf(d, 0), @@ -155,15 +167,15 @@ fn xorThenRotateRight(const_lhs: [4][4]u32, rhs: [4][4]u32, rotate: anytype) [4] return lhs; } -/// Reinterprets an integer or array of integers as an -/// integer or array of integers with different sizes. -/// For example, can convert u64 -> [2]u32 or vice versa. +/// Reinterprets an integer or array of integers as an integer or array of +/// integers with different sizes. For example, can convert u64 -> [2]u32 or vice +/// versa. /// -/// The function ensures that the resulting numbers are -/// universal across platforms, using little-endian ordering. +/// The function ensures that the resulting numbers are universal across +/// platforms, using little-endian ordering. /// -/// So, this is the same as @bitCast for little endian platforms, -/// but it requires a byte swap for big endian platforms. +/// So, this is the same as @bitCast for little endian platforms, but it requires +/// a byte swap for big endian platforms. fn leIntBitCast(comptime Output: type, input: anytype) Output { switch (endian) { .little => return @bitCast(input), @@ -214,21 +226,21 @@ fn mod(n: isize, len: usize) usize { } test "Random.int(u32) works" { - const chacha = ChaCha(20).init(.{0} ** 32); + const chacha = ChaCha(20).init(.{0} ** 32, .{0} ** 12); var rng = BlockRng(ChaCha(20), ChaCha(20).generate){ .core = chacha }; const random = rng.random(); try std.testing.expect(2917185654 == random.int(u32)); } test "Random.int(u64) works" { - const chacha = ChaCha(20).init(.{0} ** 32); + const chacha = ChaCha(20).init(.{0} ** 32, .{0} ** 12); var rng = BlockRng(ChaCha(20), ChaCha(20).generate){ .core = chacha }; const random = rng.random(); try std.testing.expect(10393729187455219830 == random.int(u64)); } test "Random.bytes works" { - const chacha = ChaCha(20).init(.{0} ** 32); + const chacha = ChaCha(20).init(.{0} ** 32, .{0} ** 12); var rng = BlockRng(ChaCha(20), ChaCha(20).generate){ .core = chacha }; const random = rng.random(); var dest: [32]u8 = undefined; @@ -242,9 +254,9 @@ test "Random.bytes works" { test "recursive fill" { var bytes: [32]u8 = .{0} ** 32; - var rng_init = ChaChaRng(20).init(bytes); - rng_init.fill(&bytes); - const chacha = ChaCha(20).init(bytes); + var rng_init = ChaChaRng(20).fromSeed(bytes); + rng_init.random().bytes(&bytes); + const chacha = ChaCha(20).init(bytes, .{0} ** 12); var rng = BlockRng(ChaCha(20), ChaCha(20).generate){ .core = chacha }; rng.fill(&bytes); @@ -257,9 +269,9 @@ test "recursive fill" { test "dynamic next int works" { var bytes: [32]u8 = .{0} ** 32; - var rng_init = ChaChaRng(20).init(bytes); - rng_init.fill(&bytes); - const chacha = ChaCha(20).init(bytes); + var rng_init = ChaChaRng(20).fromSeed(bytes); + rng_init.random().bytes(&bytes); + const chacha = ChaCha(20).init(bytes, .{0} ** 12); var rng = BlockRng(ChaCha(20), ChaCha(20).generate){ .core = chacha }; const u32s = [_]u32{ 4279565744, 862297132, 2898887311, 3678189893, 3874939098, 1553983382, 1031206440, @@ -313,7 +325,7 @@ test "dynamic next int works" { } test "rng works" { - const chacha = ChaCha(20).init(.{0} ** 32); + const chacha = ChaCha(20).init(.{0} ** 32, .{0} ** 12); var rng = BlockRng(ChaCha(20), ChaCha(20).generate){ .core = chacha }; var dest: [32]u8 = undefined; const midpoint = .{ @@ -333,7 +345,18 @@ test "ChaCha works" { }; var out: [64]u32 = .{0} ** 64; chacha.generate(&out); - const expected1 = .{ 514454965, 2343183702, 485828088, 2392727011, 3682321578, 3166467596, 1535089427, 266038024, 1861812015, 3818141583, 486852448, 277812666, 1961317633, 3870259557, 3811097870, 10333140, 3471107314, 854767140, 1292362001, 1791493576, 684928595, 2735203077, 3103536681, 1555264764, 2953779204, 1335099419, 3308039343, 3071159758, 676902921, 3409736680, 289978712, 198159109, 4106483464, 4193260066, 389599996, 1248502515, 607568078, 3047265466, 2254027974, 3837112036, 2647654845, 3933149571, 251366014, 192741632, 4239604811, 2829206891, 2090618058, 86120867, 3489155609, 162839505, 3738605468, 1369674854, 3501711964, 3507855056, 3021042483, 747171775, 3095039326, 1302941762, 1534526601, 4269591531, 2416037718, 2139104272, 3631556128, 4065100274 }; + const expected1 = .{ + 514454965, 2343183702, 485828088, 2392727011, 3682321578, 3166467596, 1535089427, + 266038024, 1861812015, 3818141583, 486852448, 277812666, 1961317633, 3870259557, + 3811097870, 10333140, 3471107314, 854767140, 1292362001, 1791493576, 684928595, + 2735203077, 3103536681, 1555264764, 2953779204, 1335099419, 3308039343, 3071159758, + 676902921, 3409736680, 289978712, 198159109, 4106483464, 4193260066, 389599996, + 1248502515, 607568078, 3047265466, 2254027974, 3837112036, 2647654845, 3933149571, + 251366014, 192741632, 4239604811, 2829206891, 2090618058, 86120867, 3489155609, + 162839505, 3738605468, 1369674854, 3501711964, 3507855056, 3021042483, 747171775, + 3095039326, 1302941762, 1534526601, 4269591531, 2416037718, 2139104272, 3631556128, + 4065100274, + }; try std.testing.expect(mem.eql(u32, &expected1, &out)); } diff --git a/src/rand/rand.zig b/src/rand/rand.zig index 8e1d9e599..645992c31 100644 --- a/src/rand/rand.zig +++ b/src/rand/rand.zig @@ -4,20 +4,23 @@ const sig = @import("../lib.zig"); const Allocator = std.mem.Allocator; const Random = std.Random; -const ChaChaRng = sig.rand.ChaCha; +const ChaChaRng = sig.rand.ChaChaRng; -/// Uniformly samples a collection of weighted items. -/// This struct only deals with the weights, and it -/// tells you which index it selects. +/// Uniformly samples a collection of weighted items. This struct only deals with +/// the weights, and it tells you which index it selects. /// -/// Each index's probability of being selected is the -/// ratio of its weight to the sum of all weights. +/// This deterministically selects the same sequence of items as WeightedIndex +/// from the rust crate rand_chacha, assuming you use a compatible pseudo-random +/// number generator. /// -/// For example, for the weights [1, 3, 2], the -/// probability of `sample` returning each index is: -/// 0 -> 1/6 -/// 1 -> 1/2 -/// 3 -> 1/3 +/// Each index's probability of being selected is the ratio of its weight to the +/// sum of all weights. +/// +/// For example, for the weights [1, 3, 2], the probability of `sample` returning +/// each index is: +/// 0. -> 1/6 +/// 1. -> 1/2 +/// 3. -> 1/3 pub fn WeightedRandomSampler(comptime uint: type) type { return struct { allocator: Allocator, @@ -27,7 +30,7 @@ pub fn WeightedRandomSampler(comptime uint: type) type { const Self = @This(); - fn init( + pub fn init( allocator: Allocator, random: Random, weights: []const uint, @@ -46,16 +49,16 @@ pub fn WeightedRandomSampler(comptime uint: type) type { }; } - fn deinit(self: Self) void { + pub fn deinit(self: Self) void { self.allocator.free(self.cumulative_weights); } /// Returns the index of the selected item - fn sample(self: *const Self) uint { - const want = self.random.uintLessThan(uint, self.total + 1); + pub fn sample(self: *const Self) uint { + const want = self.random.uintLessThan(uint, self.total); var lower: usize = 0; var upper: usize = self.cumulative_weights.len - 1; - var guess: usize = self.cumulative_weights.len * want / self.total; + var guess = upper / 2; for (0..self.cumulative_weights.len) |_| { if (self.cumulative_weights[guess] >= want) { upper = guess; @@ -72,11 +75,10 @@ pub fn WeightedRandomSampler(comptime uint: type) type { }; } -/// Wrapper for random number generators which generate -/// blocks of [64]u32. Minimizes calls to the underlying -/// random number generator by recycling unused data from -/// previous calls. Port of BlockRng from rust which -/// ensures the same sequence is generated. +/// Wrapper for random number generators which generate blocks of [64]u32. +/// Minimizes calls to the underlying random number generator by recycling unused +/// data from previous calls. Port of BlockRng from rust which ensures the same +/// sequence is generated. pub fn BlockRng( comptime T: type, comptime generate: fn (*T, *[64]u32) void, @@ -88,10 +90,6 @@ pub fn BlockRng( const Self = @This(); - pub fn init(seed: anytype) Self { - return .{ .core = @call(.auto, T.init, .{seed}) }; - } - pub fn random(self: *Self) Random { return Random.init(self, fill); } @@ -116,7 +114,7 @@ pub fn BlockRng( test "WeightedRandomSampler matches rust with chacha" { // generate data - var rng = ChaChaRng(20).init(.{0} ** 32); + var rng = ChaChaRng(20).fromSeed(.{0} ** 32); var random = rng.random(); var items: [100]u64 = undefined; for (0..100) |i| { From 828953047dd29f3f97847158c88f52e9a08c2cef Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 29 May 2024 14:23:43 -0400 Subject: [PATCH 04/28] feat(shred-collector): pass leader schedule and root slot from snapshot into shred verifier --- .gitignore | 5 --- src/accountsdb/genesis_config.zig | 16 +++------ src/cmd/cmd.zig | 47 ++++++++++++++++---------- src/core/leader_schedule.zig | 9 ++--- src/gossip/service.zig | 41 +++++++++++----------- src/shred_collector/service.zig | 18 +++++++++- src/shred_collector/shred_verifier.zig | 7 +--- test_data/.gitignore | 13 +++++++ 8 files changed, 90 insertions(+), 66 deletions(-) create mode 100644 test_data/.gitignore diff --git a/.gitignore b/.gitignore index e4af46a26..58e194d52 100644 --- a/.gitignore +++ b/.gitignore @@ -2,11 +2,6 @@ zig-cache/ zig-out/ data/ -# unpacking test snapshots -test_data/version -test_data/accounts/ -test_data/snapshots/ -test_data/tmp/ index_storage/ /gossip-dumps diff --git a/src/accountsdb/genesis_config.zig b/src/accountsdb/genesis_config.zig index e645c45b7..9124b484a 100644 --- a/src/accountsdb/genesis_config.zig +++ b/src/accountsdb/genesis_config.zig @@ -109,10 +109,10 @@ pub const EpochSchedule = extern struct { first_normal_slot: Slot, pub fn getEpoch(self: *const EpochSchedule, slot: Slot) Epoch { - return self.getEpochAndSlotIndex(slot).epoch; + return self.getEpochAndSlotIndex(slot)[0]; } - pub fn getEpochAndSlotIndex(self: *const EpochSchedule, slot: Slot) struct { epoch: Epoch, slot_index: Slot } { + pub fn getEpochAndSlotIndex(self: *const EpochSchedule, slot: Slot) struct { Epoch, Slot } { if (slot < self.first_normal_slot) { var epoch = slot +| MINIMUM_SLOTS_PER_EPOCH +| 1; epoch = @ctz(std.math.ceilPowerOfTwo(u64, epoch) catch { @@ -124,10 +124,7 @@ pub const EpochSchedule = extern struct { const slot_index = slot -| (epoch_len -| MINIMUM_SLOTS_PER_EPOCH); - return .{ - .epoch = epoch, - .slot_index = slot_index, - }; + return .{ epoch, slot_index }; } else { const normal_slot_index = slot -| self.first_normal_slot; const normal_epoch_index = std.math.divTrunc(u64, normal_slot_index, self.slots_per_epoch) catch 0; @@ -135,17 +132,14 @@ pub const EpochSchedule = extern struct { const epoch = self.first_normal_epoch +| normal_epoch_index; const slot_index = std.math.rem(u64, normal_slot_index, self.slots_per_epoch) catch 0; - return .{ - .epoch = epoch, - .slot_index = slot_index, - }; + return .{ epoch, slot_index }; } } /// get the length of the given epoch (in slots) pub fn getSlotsInEpoch(self: *const EpochSchedule, epoch: Epoch) Slot { return if (epoch < self.first_normal_epoch) - 1 <<| epoch +| @ctz(MINIMUM_SLOTS_PER_EPOCH) + @as(Slot, 1) <<| epoch +| @ctz(MINIMUM_SLOTS_PER_EPOCH) else self.slots_per_epoch; } diff --git a/src/cmd/cmd.zig b/src/cmd/cmd.zig index 5c9943889..724bd79f3 100644 --- a/src/cmd/cmd.zig +++ b/src/cmd/cmd.zig @@ -418,24 +418,11 @@ fn validator() !void { }, ); defer gossip_service.deinit(); - var gossip_handle = try spawnGossip(&gossip_service); - - // shred collector - var shred_collector = try sig.shred_collector.start(.{ - .start_slot = if (config.current.tvu.test_repair_slot) |n| @intCast(n) else null, - .repair_port = repair_port, - .tvu_port = tvu_port, - }, .{ - .allocator = gpa_allocator, - .logger = logger, - .random = rand.random(), - .my_keypair = &my_keypair, - }, .{ - .exit = &exit, - .gossip_table_rw = &gossip_service.gossip_table_rw, - .my_shred_version = &gossip_service.my_shred_version, - }); - defer shred_collector.deinit(); + var gossip_handle = try gossip_service.start( + config.current.gossip.spy_node, + config.current.gossip.dump, + ); + defer gossip_handle.deinit(); // accounts db var snapshots = try getOrDownloadSnapshots( @@ -507,6 +494,30 @@ fn validator() !void { logger.infof("accounts-db setup done...", .{}); + // shred collector + const leader_schedule = try sig.core.leaderScheduleFromBank(gpa_allocator, &bank); + _, const slot_index = bank.bank_fields.epoch_schedule.getEpochAndSlotIndex(bank.bank_fields.slot); + const epoch_start_slot = bank.bank_fields.slot - slot_index; + var shred_collector = try sig.shred_collector.start(.{ + .start_slot = if (config.current.tvu.test_repair_slot) |n| @intCast(n) else bank.bank_fields.slot, + .repair_port = repair_port, + .tvu_port = tvu_port, + }, .{ + .allocator = gpa_allocator, + .logger = logger, + .random = rand.random(), + .my_keypair = &my_keypair, + }, .{ + .exit = &exit, + .gossip_table_rw = &gossip_service.gossip_table_rw, + .my_shred_version = &gossip_service.my_shred_version, + .leader_schedule = .{ + .leader_schedule = leader_schedule, + .start_slot = epoch_start_slot, + }, + }); + defer shred_collector.deinit(); + gossip_handle.join(); shred_collector.join(); } diff --git a/src/core/leader_schedule.zig b/src/core/leader_schedule.zig index 1a160ae1f..63c22334d 100644 --- a/src/core/leader_schedule.zig +++ b/src/core/leader_schedule.zig @@ -13,11 +13,12 @@ const WeightedRandomSampler = sig.rand.WeightedRandomSampler; pub const NUM_CONSECUTIVE_LEADER_SLOTS: u64 = 4; -pub fn leaderScheduleFromBank(allocator: Allocator, bank: *const Bank, epoch: Epoch) Allocator.Error!?[]Pubkey { - const epoch_stakes = bank.bank_fields.epoch_stakes.getPtr(epoch) orelse return null; - const staked_nodes = &epoch_stakes.stakes.vote_accounts.staked_nodes orelse return null; +pub fn leaderScheduleFromBank(allocator: Allocator, bank: *const Bank) ![]Pubkey { + const epoch = bank.bank_fields.epoch; + const epoch_stakes = bank.bank_fields.epoch_stakes.getPtr(epoch) orelse return error.NoEpochStakes; + const staked_nodes = &(epoch_stakes.stakes.vote_accounts.staked_nodes orelse return error.NoStakedNodes); const slots_in_epoch = bank.bank_fields.epoch_schedule.getSlotsInEpoch(epoch); - leaderSchedule(allocator, staked_nodes, slots_in_epoch); + return try leaderSchedule(allocator, staked_nodes, slots_in_epoch, epoch); } pub fn leaderSchedule( diff --git a/src/gossip/service.zig b/src/gossip/service.zig index 56a2994a3..5a96902c6 100644 --- a/src/gossip/service.zig +++ b/src/gossip/service.zig @@ -48,6 +48,7 @@ const PingCache = @import("./ping_pong.zig").PingCache; const PingAndSocketAddr = @import("./ping_pong.zig").PingAndSocketAddr; const echo = @import("../net/echo.zig"); const GossipDumpService = @import("../gossip/dump_service.zig").GossipDumpService; +const ServiceManager = @import("../utils/service.zig").ServiceManager; const Registry = @import("../prometheus/registry.zig").Registry; const globalRegistry = @import("../prometheus/registry.zig").globalRegistry; @@ -266,7 +267,14 @@ pub const GossipService = struct { self.exit.store(true, .unordered); } - /// spawns required threads for the gossip serivce. + /// starts gossip and blocks until it exits + pub fn run(self: *Self, spy_node: bool, dump: bool) !void { + var service = try self.start(spy_node, dump); + service.join(); + service.deinit(); + } + + /// spawns required threads for the gossip service and returns immediately /// including: /// 1) socket reciever /// 2) packet verifier @@ -274,49 +282,40 @@ pub const GossipService = struct { /// 4) build message loop (to send outgoing message) (only active if not a spy node) /// 5) a socket responder (to send outgoing packets) /// 6) echo server - pub fn run(self: *Self, spy_node: bool, dump: bool) !void { + pub fn start(self: *Self, spy_node: bool, dump: bool) !ServiceManager { // TODO(Ahmad): need new server impl, for now we don't join server thread // because http.zig's server doesn't stop when you call server.stop() - it's broken // const echo_server_thread = try self.echo_server.listenAndServe(); // _ = echo_server_thread; + var services = ServiceManager.init(self.allocator, self.logger, self.exit); - var receiver_handle = try Thread.spawn(.{}, socket_utils.readSocket, .{ + try services.spawn(.{}, socket_utils.readSocket, .{ self.allocator, self.gossip_socket, self.packet_incoming_channel, self.exit, self.logger, }); - defer self.joinAndExit(&receiver_handle); - - var packet_verifier_handle = try Thread.spawn(.{}, verifyPackets, .{self}); - defer self.joinAndExit(&packet_verifier_handle); + try services.spawn(.{}, verifyPackets, .{self}); + try services.spawn(.{}, processMessages, .{self}); - var packet_handle = try Thread.spawn(.{}, processMessages, .{self}); - defer self.joinAndExit(&packet_handle); + if (!spy_node) try services.spawn(.{}, buildMessages, .{self}); - var maybe_build_messages_handle = if (!spy_node) try Thread.spawn(.{}, buildMessages, .{self}) else null; - defer { - if (maybe_build_messages_handle) |*handle| { - self.joinAndExit(handle); - } - } - - var responder_handle = try Thread.spawn(.{}, socket_utils.sendSocket, .{ + try services.spawn(.{}, socket_utils.sendSocket, .{ self.gossip_socket, self.packet_outgoing_channel, self.exit, self.logger, }); - defer self.joinAndExit(&responder_handle); - var dump_handle = if (dump) try Thread.spawn(.{}, GossipDumpService.run, .{.{ + if (dump) try services.spawn(.{}, GossipDumpService.run, .{.{ .allocator = self.allocator, .logger = self.logger, .gossip_table_rw = &self.gossip_table_rw, .exit = self.exit, - }}) else null; - defer if (dump_handle) |*h| self.joinAndExit(h); + }}); + + return services; } const VerifyMessageTask = struct { diff --git a/src/shred_collector/service.zig b/src/shred_collector/service.zig index c9f1afdc5..fc6fffd09 100644 --- a/src/shred_collector/service.zig +++ b/src/shred_collector/service.zig @@ -48,6 +48,17 @@ pub const ShredCollectorInterface = struct { gossip_table_rw: *RwMux(GossipTable), /// Shared state that is read from gossip my_shred_version: *const Atomic(u16), + leader_schedule: LeaderScheduleCalculator, +}; + +pub const LeaderScheduleCalculator = struct { + leader_schedule: []const sig.core.Pubkey, + start_slot: sig.core.Slot, + + pub fn getLeader(self: *const @This(), slot: sig.core.Slot) ?sig.core.Pubkey { + const index: usize = @intCast(slot - self.start_slot); + return if (index >= self.leader_schedule.len) null else self.leader_schedule[index]; + } }; /// Start the Shred Collector. @@ -135,7 +146,12 @@ pub fn start( try shred_collector.spawn( .{ .name = "Shred Verifier" }, sig.shred_collector.runShredSignatureVerification, - .{ interface.exit, unverified_shreds_channel, verified_shreds_channel, .{} }, + .{ + interface.exit, + unverified_shreds_channel, + verified_shreds_channel, + interface.leader_schedule, + }, ); // processor (thread) diff --git a/src/shred_collector/shred_verifier.zig b/src/shred_collector/shred_verifier.zig index 3f33f74e4..c2d2ed41a 100644 --- a/src/shred_collector/shred_verifier.zig +++ b/src/shred_collector/shred_verifier.zig @@ -7,6 +7,7 @@ const ArrayList = std.ArrayList; const Atomic = std.atomic.Value; const Channel = sig.sync.Channel; +const LeaderScheduleCalculator = sig.shred_collector.LeaderScheduleCalculator; const Packet = sig.net.Packet; /// Analogous to [run_shred_sigverify](https://github.com/anza-xyz/agave/blob/8c5a33a81a0504fd25d0465bed35d153ff84819f/turbine/src/sigverify_shreds.rs#L82) @@ -55,9 +56,3 @@ fn verifyShred(packet: *const Packet, leader_schedule: *const LeaderScheduleCalc return true; } -// TODO -pub const LeaderScheduleCalculator = struct { - fn getLeader(_: *const @This(), _: sig.core.Slot) ?sig.core.Pubkey { - return null; - } -}; diff --git a/test_data/.gitignore b/test_data/.gitignore new file mode 100644 index 000000000..968c68d77 --- /dev/null +++ b/test_data/.gitignore @@ -0,0 +1,13 @@ +# turn this file into a whitelist +* + +# the files to include in git +!.gitignore +!10 +!25 +!genesis.bin +!incremental-snapshot-10-25-GXgKvm3NMAPgGdv2verVaNXmKTHQgfy2TAxLVEfAvdCS.tar +!incremental-snapshot-10-25-GXgKvm3NMAPgGdv2verVaNXmKTHQgfy2TAxLVEfAvdCS.tar.zst +!snapshot-10-6ExseAZAVJsAZjhimxHTR7N8p6VGXiDNdsajYh1ipjAD.tar.zst +!status_cache +!test_account_file From 3094a27c3bae8bafcbc009aaaa93a8a58c9a9ec9 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 29 May 2024 16:34:00 -0400 Subject: [PATCH 05/28] refactor(utils): more versatile ServiceManager --- src/gossip/service.zig | 16 ++-- src/shred_collector/repair_service.zig | 1 - src/shred_collector/service.zig | 12 +-- src/utils/service.zig | 104 +++++++++++++++---------- 4 files changed, 78 insertions(+), 55 deletions(-) diff --git a/src/gossip/service.zig b/src/gossip/service.zig index 5a96902c6..afb6022d7 100644 --- a/src/gossip/service.zig +++ b/src/gossip/service.zig @@ -287,35 +287,35 @@ pub const GossipService = struct { // because http.zig's server doesn't stop when you call server.stop() - it's broken // const echo_server_thread = try self.echo_server.listenAndServe(); // _ = echo_server_thread; - var services = ServiceManager.init(self.allocator, self.logger, self.exit); + var manager = ServiceManager.init(self.allocator, self.logger, self.exit, .{}, .{}); - try services.spawn(.{}, socket_utils.readSocket, .{ + try manager.spawn("gossip readSocket", socket_utils.readSocket, .{ self.allocator, self.gossip_socket, self.packet_incoming_channel, self.exit, self.logger, }); - try services.spawn(.{}, verifyPackets, .{self}); - try services.spawn(.{}, processMessages, .{self}); + try manager.spawn("gossip verifyPackets", verifyPackets, .{self}); + try manager.spawn("gossip processMessages", processMessages, .{self}); - if (!spy_node) try services.spawn(.{}, buildMessages, .{self}); + if (!spy_node) try manager.spawn("gossip buildMessages", buildMessages, .{self}); - try services.spawn(.{}, socket_utils.sendSocket, .{ + try manager.spawn("gossip sendSocket", socket_utils.sendSocket, .{ self.gossip_socket, self.packet_outgoing_channel, self.exit, self.logger, }); - if (dump) try services.spawn(.{}, GossipDumpService.run, .{.{ + if (dump) try manager.spawn("GossipDumpService", GossipDumpService.run, .{.{ .allocator = self.allocator, .logger = self.logger, .gossip_table_rw = &self.gossip_table_rw, .exit = self.exit, }}); - return services; + return manager; } const VerifyMessageTask = struct { diff --git a/src/shred_collector/repair_service.zig b/src/shred_collector/repair_service.zig index 8fe9440ea..283ffed0b 100644 --- a/src/shred_collector/repair_service.zig +++ b/src/shred_collector/repair_service.zig @@ -95,7 +95,6 @@ pub const RepairService = struct { /// Used to run RepairService continuously. pub const run_config = sig.utils.RunConfig{ - .name = "Repair Service", .min_loop_duration_ns = 100 * std.time.ns_per_ms, }; diff --git a/src/shred_collector/service.zig b/src/shred_collector/service.zig index fc6fffd09..71e0cf231 100644 --- a/src/shred_collector/service.zig +++ b/src/shred_collector/service.zig @@ -75,7 +75,7 @@ pub fn start( deps: ShredCollectorDependencies, interface: ShredCollectorInterface, ) !ServiceManager { - var shred_collector = ServiceManager.init(deps.allocator, deps.logger, interface.exit); + var shred_collector = ServiceManager.init(deps.allocator, deps.logger, interface.exit, .{}, .{}); var arena = shred_collector.arena(); const repair_socket = try bindUdpReusable(conf.repair_port); @@ -114,8 +114,10 @@ pub fn start( repair_peer_provider, shred_tracker, ); - try shred_collector.spawn( + try shred_collector.spawnCustom( + "Repair Service", RepairService.run_config, + .{}, RepairService.sendNecessaryRepairs, .{repair_svc}, ); @@ -140,11 +142,11 @@ pub fn start( .outgoing_shred_channel = unverified_shreds_channel, .shred_version = interface.my_shred_version, }; - try shred_collector.spawn(.{ .name = "Shred Receiver" }, ShredReceiver.run, .{shred_receiver}); + try shred_collector.spawn("Shred Receiver", ShredReceiver.run, .{shred_receiver}); // verifier (thread) try shred_collector.spawn( - .{ .name = "Shred Verifier" }, + "Shred Verifier", sig.shred_collector.runShredSignatureVerification, .{ interface.exit, @@ -156,7 +158,7 @@ pub fn start( // processor (thread) try shred_collector.spawn( - .{ .name = "Shred Processor" }, + "Shred Processor", sig.shred_collector.processShreds, .{ deps.allocator, verified_shreds_channel, shred_tracker }, ); diff --git a/src/utils/service.zig b/src/utils/service.zig index 5c5290a7f..d694edabf 100644 --- a/src/utils/service.zig +++ b/src/utils/service.zig @@ -25,16 +25,26 @@ pub const ServiceManager = struct { _arena: ArenaAllocator, /// Logic to run after all threads join. defers: DeferList, + default_run_config: RunConfig, + default_spawn_config: std.Thread.SpawnConfig, const Self = @This(); - pub fn init(allocator: Allocator, logger: Logger, exit: *Atomic(bool)) Self { + pub fn init( + allocator: Allocator, + logger: Logger, + exit: *Atomic(bool), + default_run_config: RunConfig, + default_spawn_config: std.Thread.SpawnConfig, + ) Self { return .{ .logger = logger, .exit = exit, .threads = std.ArrayList(std.Thread).init(allocator), ._arena = ArenaAllocator.init(allocator), .defers = DeferList.init(allocator), + .default_run_config = default_run_config, + .default_spawn_config = default_spawn_config, }; } @@ -50,19 +60,32 @@ pub const ServiceManager = struct { } /// Spawn a thread to be managed. - /// The function may be restarted periodically, according to the config. + /// The function may be restarted periodically, according to default_run_config. pub fn spawn( self: *Self, - config: RunConfig, + name: ?[]const u8, + comptime function: anytype, + args: anytype, + ) !void { + return self.spawnCustom(name, self.default_run_config, self.default_spawn_config, function, args); + } + + /// Spawn a thread to be managed. + /// The function may be restarted periodically, according to the provided config. + pub fn spawnCustom( + self: *Self, + maybe_name: ?[]const u8, + run_config: ?RunConfig, + spawn_config: std.Thread.SpawnConfig, comptime function: anytype, args: anytype, ) !void { var thread = try std.Thread.spawn( - .{}, + spawn_config, runService, - .{ self.logger, self.exit, config, function, args }, + .{ self.logger, self.exit, maybe_name, run_config orelse self.default_run_config, function, args }, ); - if (config.name) |name| thread.setName(name) catch {}; + if (maybe_name) |name| thread.setName(name) catch {}; try self.threads.append(thread); } @@ -85,11 +108,10 @@ pub const ServiceManager = struct { }; pub const RunConfig = struct { - name: ?[]const u8 = null, /// what to do when the task returns without error - return_handler: ReturnHandler = .keep_looping, - /// what to do when the task returns with an error - error_handler: ReturnHandler = .keep_looping, + return_handler: ReturnHandler = .{ .log_return = false }, + /// what to do when the task returns an error + error_handler: ReturnHandler = .{}, /// The minimum amount of time to spend on the entire loop, /// including the logic plus the pause. min_loop_duration_ns: u64 = 0, @@ -98,10 +120,17 @@ pub const RunConfig = struct { min_pause_ns: u64 = 0, }; -pub const ReturnHandler = enum { - keep_looping, - just_return, - set_exit_and_return, +pub const ReturnHandler = struct { + /// Loop the task until the return event occurs this many times. + /// null means an infinite loop. + max_iterations: ?u64 = null, + /// Whether to set the `exit` bool to true after max_iterations + /// is reached. + set_exit_on_completion: bool = false, + /// Whether to log after each return. + log_return: bool = true, + /// Whether to log when exiting on the final return. + log_exit: bool = true, }; /// Convert a short-lived task into a long-lived service by looping it, @@ -109,12 +138,13 @@ pub const ReturnHandler = enum { pub fn runService( logger: Logger, exit: *Atomic(bool), + maybe_name: ?[]const u8, config: RunConfig, function: anytype, args: anytype, ) !void { var buf: [16]u8 = undefined; - const name = config.name orelse try std.fmt.bufPrint( + const name = maybe_name orelse try std.fmt.bufPrint( &buf, "thread {d}", .{std.Thread.getCurrentId()}, @@ -122,34 +152,26 @@ pub fn runService( logger.infof("Starting {s}", .{name}); var timer = try std.time.Timer.start(); var last_iteration: u64 = 0; + var num_oks: u64 = 0; + var num_errors: u64 = 0; while (!exit.load(.unordered)) { - if (@call(.auto, function, args)) |ok| { - switch (config.error_handler) { - .keep_looping => {}, - .just_return => { - logger.errf("Exiting {s} due to return", .{name}); - return ok; - }, - .set_exit_and_return => { - logger.errf("Signalling exit due to return from {s}", .{name}); - exit.store(true, .monotonic); - return ok; - }, - } - } else |err| { - switch (config.error_handler) { - .keep_looping => logger.errf("Unhandled error in {s}: {}", .{ name, err }), - .just_return => { - logger.errf("Exiting {s} due to error: {}", .{ name, err }); - return err; - }, - .set_exit_and_return => { - logger.errf("Signalling exit due to error in {s}: {}", .{ name, err }); - exit.store(true, .monotonic); - return err; - }, + const result = @call(.auto, function, args); + if (result) |_| num_oks += 1 else |_| num_errors += 1; + const handler, const num_events, const event_name, const err: ?anyerror = if (result) |_| + .{ config.return_handler, num_oks, "returned", null } + else |err| + .{ config.error_handler, num_errors, "error", err }; + if (handler.log_return) logger.errf("{s} has {s}ed: {?}", .{ name, event_name, err }); + if (handler.max_iterations) |max| if (num_events >= max) { + if (handler.set_exit_on_completion) { + if (handler.log_exit) logger.errf("Signaling exit due to {}th {s} from {s}", .{ num_events, event_name, name }); + exit.store(true, .monotonic); + } else { + if (handler.log_exit) logger.errf("Exiting {s} due to {}th {s}", .{ name, num_events, event_name }); } - } + return result; + }; + last_iteration = timer.lap(); std.time.sleep(@max( config.min_pause_ns, From 6f7b491e9fdc723f2552b66584e7537d8beccb5c Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 29 May 2024 19:38:20 -0400 Subject: [PATCH 06/28] feat(utils): log message when cleaning up service manager --- src/gossip/service.zig | 2 +- src/shred_collector/service.zig | 2 +- src/utils/service.zig | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/gossip/service.zig b/src/gossip/service.zig index afb6022d7..3ff1e6eb6 100644 --- a/src/gossip/service.zig +++ b/src/gossip/service.zig @@ -287,7 +287,7 @@ pub const GossipService = struct { // because http.zig's server doesn't stop when you call server.stop() - it's broken // const echo_server_thread = try self.echo_server.listenAndServe(); // _ = echo_server_thread; - var manager = ServiceManager.init(self.allocator, self.logger, self.exit, .{}, .{}); + var manager = ServiceManager.init("gossip", self.allocator, self.logger, self.exit, .{}, .{}); try manager.spawn("gossip readSocket", socket_utils.readSocket, .{ self.allocator, diff --git a/src/shred_collector/service.zig b/src/shred_collector/service.zig index 71e0cf231..5cc3653b3 100644 --- a/src/shred_collector/service.zig +++ b/src/shred_collector/service.zig @@ -75,7 +75,7 @@ pub fn start( deps: ShredCollectorDependencies, interface: ShredCollectorInterface, ) !ServiceManager { - var shred_collector = ServiceManager.init(deps.allocator, deps.logger, interface.exit, .{}, .{}); + var shred_collector = ServiceManager.init("shred collector", deps.allocator, deps.logger, interface.exit, .{}, .{}); var arena = shred_collector.arena(); const repair_socket = try bindUdpReusable(conf.repair_port); diff --git a/src/utils/service.zig b/src/utils/service.zig index d694edabf..220bba063 100644 --- a/src/utils/service.zig +++ b/src/utils/service.zig @@ -25,6 +25,7 @@ pub const ServiceManager = struct { _arena: ArenaAllocator, /// Logic to run after all threads join. defers: DeferList, + name: ?[]const u8, default_run_config: RunConfig, default_spawn_config: std.Thread.SpawnConfig, @@ -34,6 +35,7 @@ pub const ServiceManager = struct { allocator: Allocator, logger: Logger, exit: *Atomic(bool), + name: ?[]const u8, default_run_config: RunConfig, default_spawn_config: std.Thread.SpawnConfig, ) Self { @@ -43,6 +45,7 @@ pub const ServiceManager = struct { .threads = std.ArrayList(std.Thread).init(allocator), ._arena = ArenaAllocator.init(allocator), .defers = DeferList.init(allocator), + .name = name, .default_run_config = default_run_config, .default_spawn_config = default_spawn_config, }; @@ -99,11 +102,13 @@ pub const ServiceManager = struct { /// 2. Wait for threads to exit. /// 3. Deinit the shared state from those threads. pub fn deinit(self: Self) void { + self.logger.infof("Cleaning up: {}", .{self.name}); self.exit.store(true, .monotonic); for (self.threads.items) |t| t.join(); self.threads.deinit(); self.defers.deinit(); self._arena.deinit(); + self.logger.infof("Finished cleaning up: {}", .{self.name}); } }; From 9cf54b19f5fda377031eb40d8b662cb707553591 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 29 May 2024 19:42:08 -0400 Subject: [PATCH 07/28] fix(utils): service manager name --- src/gossip/service.zig | 2 +- src/shred_collector/service.zig | 2 +- src/utils/service.zig | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/gossip/service.zig b/src/gossip/service.zig index 3ff1e6eb6..7a10c668e 100644 --- a/src/gossip/service.zig +++ b/src/gossip/service.zig @@ -287,7 +287,7 @@ pub const GossipService = struct { // because http.zig's server doesn't stop when you call server.stop() - it's broken // const echo_server_thread = try self.echo_server.listenAndServe(); // _ = echo_server_thread; - var manager = ServiceManager.init("gossip", self.allocator, self.logger, self.exit, .{}, .{}); + var manager = ServiceManager.init(self.allocator, self.logger, self.exit, "gossip", .{}, .{}); try manager.spawn("gossip readSocket", socket_utils.readSocket, .{ self.allocator, diff --git a/src/shred_collector/service.zig b/src/shred_collector/service.zig index 5cc3653b3..1185fabb1 100644 --- a/src/shred_collector/service.zig +++ b/src/shred_collector/service.zig @@ -75,7 +75,7 @@ pub fn start( deps: ShredCollectorDependencies, interface: ShredCollectorInterface, ) !ServiceManager { - var shred_collector = ServiceManager.init("shred collector", deps.allocator, deps.logger, interface.exit, .{}, .{}); + var shred_collector = ServiceManager.init(deps.allocator, deps.logger, interface.exit, "shred collector", .{}, .{}); var arena = shred_collector.arena(); const repair_socket = try bindUdpReusable(conf.repair_port); diff --git a/src/utils/service.zig b/src/utils/service.zig index 220bba063..5e47817be 100644 --- a/src/utils/service.zig +++ b/src/utils/service.zig @@ -25,7 +25,7 @@ pub const ServiceManager = struct { _arena: ArenaAllocator, /// Logic to run after all threads join. defers: DeferList, - name: ?[]const u8, + name: []const u8, default_run_config: RunConfig, default_spawn_config: std.Thread.SpawnConfig, @@ -35,7 +35,7 @@ pub const ServiceManager = struct { allocator: Allocator, logger: Logger, exit: *Atomic(bool), - name: ?[]const u8, + name: []const u8, default_run_config: RunConfig, default_spawn_config: std.Thread.SpawnConfig, ) Self { @@ -102,13 +102,13 @@ pub const ServiceManager = struct { /// 2. Wait for threads to exit. /// 3. Deinit the shared state from those threads. pub fn deinit(self: Self) void { - self.logger.infof("Cleaning up: {}", .{self.name}); + self.logger.infof("Cleaning up: {s}", .{self.name}); self.exit.store(true, .monotonic); for (self.threads.items) |t| t.join(); self.threads.deinit(); self.defers.deinit(); self._arena.deinit(); - self.logger.infof("Finished cleaning up: {}", .{self.name}); + self.logger.infof("Finished cleaning up: {s}", .{self.name}); } }; From 58e49e358f05b735ad5f2ef5bd9ce56c35108f38 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 29 May 2024 20:09:38 -0400 Subject: [PATCH 08/28] fix(utils): log unexpected returns by default --- src/shred_collector/repair_service.zig | 1 + src/utils/service.zig | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/shred_collector/repair_service.zig b/src/shred_collector/repair_service.zig index 283ffed0b..e265b648a 100644 --- a/src/shred_collector/repair_service.zig +++ b/src/shred_collector/repair_service.zig @@ -95,6 +95,7 @@ pub const RepairService = struct { /// Used to run RepairService continuously. pub const run_config = sig.utils.RunConfig{ + .return_handler = .{ .log_return = false }, .min_loop_duration_ns = 100 * std.time.ns_per_ms, }; diff --git a/src/utils/service.zig b/src/utils/service.zig index 5e47817be..64eba2b2d 100644 --- a/src/utils/service.zig +++ b/src/utils/service.zig @@ -114,7 +114,7 @@ pub const ServiceManager = struct { pub const RunConfig = struct { /// what to do when the task returns without error - return_handler: ReturnHandler = .{ .log_return = false }, + return_handler: ReturnHandler = .{}, /// what to do when the task returns an error error_handler: ReturnHandler = .{}, /// The minimum amount of time to spend on the entire loop, From c1a7e399317de7e48cca21fdf18bef5c1e93cc96 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 29 May 2024 20:12:05 -0400 Subject: [PATCH 09/28] feat(utils): log service exit when receiving exit signal --- src/utils/service.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/service.zig b/src/utils/service.zig index 64eba2b2d..50813b3e8 100644 --- a/src/utils/service.zig +++ b/src/utils/service.zig @@ -183,6 +183,7 @@ pub fn runService( config.min_loop_duration_ns -| last_iteration, )); } + logger.infof("Exiting {s} due to exit signal received", .{name}); } /// Defer actions until later. From cd4197893ff414d33d4806fdd958c47c2d151325 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Thu, 30 May 2024 22:01:23 -0400 Subject: [PATCH 10/28] refactor(leader_schedule): generify leader schedule and make it required --- src/cmd/cmd.zig | 14 +++++++----- src/core/leader_schedule.zig | 20 +++++++++++++++++ src/lib.zig | 1 + src/shred_collector/service.zig | 13 ++--------- src/shred_collector/shred_verifier.zig | 16 +++++-------- src/utils/closure.zig | 31 ++++++++++++++++++++++++++ src/utils/service.zig | 4 ++-- 7 files changed, 71 insertions(+), 28 deletions(-) create mode 100644 src/utils/closure.zig diff --git a/src/cmd/cmd.zig b/src/cmd/cmd.zig index 724bd79f3..8082dc7ff 100644 --- a/src/cmd/cmd.zig +++ b/src/cmd/cmd.zig @@ -494,10 +494,17 @@ fn validator() !void { logger.infof("accounts-db setup done...", .{}); - // shred collector + // leader schedule const leader_schedule = try sig.core.leaderScheduleFromBank(gpa_allocator, &bank); _, const slot_index = bank.bank_fields.epoch_schedule.getEpochAndSlotIndex(bank.bank_fields.slot); const epoch_start_slot = bank.bank_fields.slot - slot_index; + var slot_leader_getter = sig.core.SingleEpochLeaderSchedule{ + .leader_schedule = leader_schedule, + .start_slot = epoch_start_slot, + }; + const getter = slot_leader_getter.provider(); + + // shred collector var shred_collector = try sig.shred_collector.start(.{ .start_slot = if (config.current.tvu.test_repair_slot) |n| @intCast(n) else bank.bank_fields.slot, .repair_port = repair_port, @@ -511,10 +518,7 @@ fn validator() !void { .exit = &exit, .gossip_table_rw = &gossip_service.gossip_table_rw, .my_shred_version = &gossip_service.my_shred_version, - .leader_schedule = .{ - .leader_schedule = leader_schedule, - .start_slot = epoch_start_slot, - }, + .leader_schedule = getter, }); defer shred_collector.deinit(); diff --git a/src/core/leader_schedule.zig b/src/core/leader_schedule.zig index 63c22334d..93ec8d89b 100644 --- a/src/core/leader_schedule.zig +++ b/src/core/leader_schedule.zig @@ -13,6 +13,26 @@ const WeightedRandomSampler = sig.rand.WeightedRandomSampler; pub const NUM_CONSECUTIVE_LEADER_SLOTS: u64 = 4; +pub const SlotLeaderProvider = sig.utils.PointerClosure(Slot, ?Pubkey); + +/// Only works for a single epoch. This is a basic limited approach that should +/// only be used as a placeholder until a better approach is fleshed out. +pub const SingleEpochLeaderSchedule = struct { + leader_schedule: []const sig.core.Pubkey, + start_slot: sig.core.Slot, + + const Self = @This(); + + pub fn getLeader(self: *const Self, slot: sig.core.Slot) ?sig.core.Pubkey { + const index: usize = @intCast(slot - self.start_slot); + return if (index >= self.leader_schedule.len) null else self.leader_schedule[index]; + } + + pub fn provider(self: *Self) SlotLeaderProvider { + return SlotLeaderProvider.init(self, Self.getLeader); + } +}; + pub fn leaderScheduleFromBank(allocator: Allocator, bank: *const Bank) ![]Pubkey { const epoch = bank.bank_fields.epoch; const epoch_stakes = bank.bank_fields.epoch_stakes.getPtr(epoch) orelse return error.NoEpochStakes; diff --git a/src/lib.zig b/src/lib.zig index 0342cd87f..4901000b8 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -64,6 +64,7 @@ pub const sync = struct { pub const utils = struct { pub usingnamespace @import("utils/arraylist.zig"); pub usingnamespace @import("utils/bitflags.zig"); + pub usingnamespace @import("utils/closure.zig"); pub usingnamespace @import("utils/lazy.zig"); pub usingnamespace @import("utils/math.zig"); pub usingnamespace @import("utils/shortvec.zig"); diff --git a/src/shred_collector/service.zig b/src/shred_collector/service.zig index 1185fabb1..ea2d02e8c 100644 --- a/src/shred_collector/service.zig +++ b/src/shred_collector/service.zig @@ -14,6 +14,7 @@ const Pubkey = sig.core.Pubkey; const RwMux = sig.sync.RwMux; const ServiceManager = sig.utils.ServiceManager; const Slot = sig.core.Slot; +const SlotLeaderGetter = sig.core.SlotLeaderProvider; const this = sig.shred_collector; const BasicShredTracker = this.BasicShredTracker; @@ -48,17 +49,7 @@ pub const ShredCollectorInterface = struct { gossip_table_rw: *RwMux(GossipTable), /// Shared state that is read from gossip my_shred_version: *const Atomic(u16), - leader_schedule: LeaderScheduleCalculator, -}; - -pub const LeaderScheduleCalculator = struct { - leader_schedule: []const sig.core.Pubkey, - start_slot: sig.core.Slot, - - pub fn getLeader(self: *const @This(), slot: sig.core.Slot) ?sig.core.Pubkey { - const index: usize = @intCast(slot - self.start_slot); - return if (index >= self.leader_schedule.len) null else self.leader_schedule[index]; - } + leader_schedule: SlotLeaderGetter, }; /// Start the Shred Collector. diff --git a/src/shred_collector/shred_verifier.zig b/src/shred_collector/shred_verifier.zig index c2d2ed41a..d09f96a13 100644 --- a/src/shred_collector/shred_verifier.zig +++ b/src/shred_collector/shred_verifier.zig @@ -7,7 +7,7 @@ const ArrayList = std.ArrayList; const Atomic = std.atomic.Value; const Channel = sig.sync.Channel; -const LeaderScheduleCalculator = sig.shred_collector.LeaderScheduleCalculator; +const SlotLeaderGetter = sig.core.SlotLeaderProvider; const Packet = sig.net.Packet; /// Analogous to [run_shred_sigverify](https://github.com/anza-xyz/agave/blob/8c5a33a81a0504fd25d0465bed35d153ff84819f/turbine/src/sigverify_shreds.rs#L82) @@ -15,7 +15,7 @@ pub fn runShredSignatureVerification( exit: *Atomic(bool), incoming: *Channel(ArrayList(Packet)), verified: *Channel(ArrayList(Packet)), - leader_schedule: LeaderScheduleCalculator, + leader_schedule: SlotLeaderGetter, ) !void { var verified_count: usize = 0; var buf: ArrayList(ArrayList(Packet)) = ArrayList(ArrayList(Packet)).init(incoming.allocator); @@ -28,7 +28,7 @@ pub fn runShredSignatureVerification( for (buf.items) |packet_batch| { // TODO parallelize this once it's actually verifying signatures for (packet_batch.items) |*packet| { - if (!verifyShred(packet, &leader_schedule)) { + if (!verifyShred(packet, leader_schedule)) { packet.set(.discard); } else { verified_count += 1; @@ -41,18 +41,14 @@ pub fn runShredSignatureVerification( } /// verify_shred_cpu -fn verifyShred(packet: *const Packet, leader_schedule: *const LeaderScheduleCalculator) bool { +fn verifyShred(packet: *const Packet, leader_schedule: SlotLeaderGetter) bool { if (packet.isSet(.discard)) return false; const shred = shred_layout.getShred(packet) orelse return false; const slot = shred_layout.getSlot(shred) orelse return false; const signature = shred_layout.getSignature(shred) orelse return false; const signed_data = shred_layout.getSignedData(shred) orelse return false; - // TODO: once implemented, this should no longer be optional - if (leader_schedule.getLeader(slot)) |leader| { - return signature.verify(leader, &signed_data.data); - } + const leader = leader_schedule.call(slot) orelse return false; - return true; + return signature.verify(leader, &signed_data.data); } - diff --git a/src/utils/closure.zig b/src/utils/closure.zig new file mode 100644 index 000000000..e8e0763c6 --- /dev/null +++ b/src/utils/closure.zig @@ -0,0 +1,31 @@ +/// Generically represents a function with two inputs: +/// 1. enclosed state that is passed on initialization (as a pointer). +/// 2. input that is passed at the call site. +/// +/// The enclosed state's type is abstracted with dynamic dispatch. +/// +/// Contains a pointer to data that is owned by another context. Ensure that +/// the lifetime of that data exceeds the lifetime of this struct. +pub fn PointerClosure(comptime Input: type, comptime Output: type) type { + return struct { + state: *anyopaque, + genericFn: *const fn (*anyopaque, Input) Output, + + const Self = @This(); + + pub fn init(state: anytype, getFn: fn (@TypeOf(state), Input) Output) Self { + return .{ + .state = @alignCast(@ptrCast(state)), + .genericFn = struct { + fn callGeneric(generic_state: *anyopaque, slot: Input) Output { + return getFn(@alignCast(@ptrCast(generic_state)), slot); + } + }.callGeneric, + }; + } + + pub fn call(self: Self, slot: Input) Output { + return self.genericFn(self.state, slot); + } + }; +} diff --git a/src/utils/service.zig b/src/utils/service.zig index 50813b3e8..cc9724620 100644 --- a/src/utils/service.zig +++ b/src/utils/service.zig @@ -163,10 +163,10 @@ pub fn runService( const result = @call(.auto, function, args); if (result) |_| num_oks += 1 else |_| num_errors += 1; const handler, const num_events, const event_name, const err: ?anyerror = if (result) |_| - .{ config.return_handler, num_oks, "returned", null } + .{ config.return_handler, num_oks, "return", null } else |err| .{ config.error_handler, num_errors, "error", err }; - if (handler.log_return) logger.errf("{s} has {s}ed: {?}", .{ name, event_name, err }); + if (handler.log_return) logger.errf("{s} has {s}ed: {}", .{ name, event_name, err orelse error.no_error }); if (handler.max_iterations) |max| if (num_events >= max) { if (handler.set_exit_on_completion) { if (handler.log_exit) logger.errf("Signaling exit due to {}th {s} from {s}", .{ num_events, event_name, name }); From 81886b8cfc7c1e4d91aace7334350aa40069dab1 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Mon, 3 Jun 2024 17:55:55 -0400 Subject: [PATCH 11/28] fix(shred-collector): staked nodes & signed data - staked_nodes deserialization is not supported so i get the same data from vote_accounts - implementation for getSignedData was missing so i added that plus a bunch of supporting functions - also made some minor improvements to logger and service manager --- src/core/leader_schedule.zig | 47 ++++---- src/shred_collector/shred.zig | 201 +++++++++++++++++++++++++--------- src/trace/log.zig | 18 +++ src/utils/service.zig | 36 ++++-- 4 files changed, 218 insertions(+), 84 deletions(-) diff --git a/src/core/leader_schedule.zig b/src/core/leader_schedule.zig index 93ec8d89b..28ea066d6 100644 --- a/src/core/leader_schedule.zig +++ b/src/core/leader_schedule.zig @@ -36,33 +36,37 @@ pub const SingleEpochLeaderSchedule = struct { pub fn leaderScheduleFromBank(allocator: Allocator, bank: *const Bank) ![]Pubkey { const epoch = bank.bank_fields.epoch; const epoch_stakes = bank.bank_fields.epoch_stakes.getPtr(epoch) orelse return error.NoEpochStakes; - const staked_nodes = &(epoch_stakes.stakes.vote_accounts.staked_nodes orelse return error.NoStakedNodes); const slots_in_epoch = bank.bank_fields.epoch_schedule.getSlotsInEpoch(epoch); + + const vote_accounts = epoch_stakes.stakes.vote_accounts.vote_accounts; + const staked_nodes = try allocator.alloc(StakedNode, vote_accounts.count()); + defer allocator.free(staked_nodes); + var iter = vote_accounts.iterator(); + var index: usize = 0; + while (iter.next()) |entry| : (index += 1) { + staked_nodes[index] = .{ + .id = entry.key_ptr.*, + .stake = entry.value_ptr.*[0], + }; + } + return try leaderSchedule(allocator, staked_nodes, slots_in_epoch, epoch); } +pub const StakedNode = struct { id: Pubkey, stake: u64 }; + pub fn leaderSchedule( allocator: Allocator, - staked_nodes: *const std.AutoHashMap(Pubkey, u64), + nodes: []StakedNode, slots_in_epoch: Slot, epoch: Epoch, ) Allocator.Error![]Pubkey { - const Entry = std.AutoHashMap(Pubkey, u64).Entry; - - // sort nodes descending by stake weight, then pubkey - const nodes = try allocator.alloc(Entry, staked_nodes.count()); - defer allocator.free(nodes); - var iter = staked_nodes.iterator(); - var index: usize = 0; - while (iter.next()) |staked_node_entry| : (index += 1) { - nodes[index] = staked_node_entry; - } - std.mem.sortUnstable(Entry, nodes, {}, struct { - fn gt(_: void, lhs: Entry, rhs: Entry) bool { - return switch (std.math.order(lhs.value_ptr.*, rhs.value_ptr.*)) { + std.mem.sortUnstable(StakedNode, nodes, {}, struct { + fn gt(_: void, lhs: StakedNode, rhs: StakedNode) bool { + return switch (std.math.order(lhs.stake, rhs.stake)) { .gt => true, .lt => false, - .eq => .gt == std.mem.order(u8, &lhs.key_ptr.data, &rhs.key_ptr.data), + .eq => .gt == std.mem.order(u8, &lhs.id.data, &rhs.id.data), }; } }.gt); @@ -76,7 +80,7 @@ pub fn leaderSchedule( // init sampler from stake weights const stakes = try allocator.alloc(u64, nodes.len); defer allocator.free(stakes); - for (nodes, 0..) |entry, i| stakes[i] = entry.value_ptr.*; + for (nodes, 0..) |entry, i| stakes[i] = entry.stake; var sampler = try WeightedRandomSampler(u64).init(allocator, random, stakes); defer sampler.deinit(); @@ -85,7 +89,7 @@ pub fn leaderSchedule( var current_node: Pubkey = undefined; for (0..slots_in_epoch) |i| { if (i % NUM_CONSECUTIVE_LEADER_SLOTS == 0) { - current_node = nodes[sampler.sample()].key_ptr.*; + current_node = nodes[sampler.sample()].id; } slot_leaders[i] = current_node; } @@ -97,13 +101,12 @@ test "leaderSchedule calculation matches agave" { var rng = ChaChaRng(20).fromSeed(.{0} ** 32); const random = rng.random(); var pubkey_bytes: [32]u8 = undefined; - var staked_nodes = std.AutoHashMap(Pubkey, u64).init(std.testing.allocator); - defer staked_nodes.deinit(); - for (0..100) |_| { + var staked_nodes: [100]StakedNode = undefined; + for (0..100) |i| { random.bytes(&pubkey_bytes); const key = Pubkey{ .data = pubkey_bytes }; const stake = random.int(u64) / 1000; - try staked_nodes.put(key, stake); + staked_nodes[i] = .{ .id = key, .stake = stake }; } const slot_leaders = try leaderSchedule(std.testing.allocator, &staked_nodes, 321, 123); defer std.testing.allocator.free(slot_leaders); diff --git a/src/shred_collector/shred.zig b/src/shred_collector/shred.zig index 825831713..6e2ef80d2 100644 --- a/src/shred_collector/shred.zig +++ b/src/shred_collector/shred.zig @@ -219,47 +219,17 @@ pub fn GenericShred( } } - /// TODO should this be memoized? - fn capacity(proof_size: u8, chained: bool, resigned: bool) !usize { - std.debug.assert(chained or !resigned); - return checkedSub( - constants.payload_size, - constants.headers_size + - if (chained) SIZE_OF_MERKLE_ROOT else 0 + - proof_size * merkle_proof_entry_size + - if (resigned) SIGNATURE_LENGTH else 0, - ) catch error.InvalidProofSize; - } - /// The return contains a pointer to data owned by the shred. fn merkleProof(self: *const Self) !MerkleProofEntryList { - const size = self.common.shred_variant.proof_size * merkle_proof_entry_size; - const offset = try self.proofOffset(); - const end = offset + size; - if (self.payload.len < end) { - return error.InsufficentPayloadSize; - } - return .{ - .bytes = self.payload[offset..end], - .len = self.common.shred_variant.proof_size, - }; - } - - // Where the merkle proof starts in the shred binary. - fn proofOffset(self: *const Self) !usize { - const v = self.common.shred_variant; - return constants.headers_size + - try capacity(v.proof_size, v.chained, v.resigned) + - if (v.chained) SIZE_OF_MERKLE_ROOT else 0; + return getMerkleProof(self.payload, constants, self.common.shred_variant); } fn erasureShardAsSlice(self: *const Self) ![]u8 { if (self.payload.len() != self.constants().payload_size) { return error.InvalidPayloadSize; } - const variant = self.common.shred_variant; const end = constants.headers_size + - try capacity(variant.proof_size, variant.chained, variant.resigned) + + try capacity(self.common.shred_variant) + SIGNATURE_LENGTH; if (self.payload.len < end) { return error.InsufficientPayloadSize; @@ -269,19 +239,146 @@ pub fn GenericShred( }; } +fn getMerkleRoot( + shred: []const u8, + constants: ShredConstants, + variant: ShredVariant, +) !Hash { + const index = switch (variant.shred_type) { + .Code => codeIndex(shred) orelse return error.InvalidErasureShardIndex, + .Data => dataIndex(shred) orelse return error.InvalidErasureShardIndex, + }; + const proof = try getMerkleProof(shred, constants, variant); + const offset = try proofOffset(constants, variant); + const node = try getMerkleNode(shred, SIGNATURE_LENGTH, offset); + return calculateMerkleRoot(index, node, proof); +} + +fn getMerkleProof( + shred: []const u8, + constants: ShredConstants, + variant: ShredVariant, +) !MerkleProofEntryList { + const size = variant.proof_size * merkle_proof_entry_size; + const offset = try proofOffset(constants, variant); + const end = offset + size; + if (shred.len < end) { + return error.InsufficentPayloadSize; + } + return .{ + .bytes = shred[offset..end], + .len = variant.proof_size, + }; +} + +fn getMerkleNode(shred: []const u8, start: usize, end: usize) !Hash { + if (shred.len < end) return error.InvalidPayloadSize; + return hashv(&.{ MERKLE_HASH_PREFIX_LEAF, shred[start..end] }); +} + +fn calculateMerkleRoot(start_index: usize, start_node: Hash, proof: MerkleProofEntryList) !Hash { + var index = start_index; + var node = start_node; + var iterator = proof.iterator(); + while (iterator.next()) |other| { + node = if (index % 2 == 0) + joinNodes(&node.data, &other) + else + joinNodes(&other, &node.data); + index = index >> 1; + } + if (index == 0) return error.InvalidMerkleProf; + return node; +} + +const MERKLE_HASH_PREFIX_LEAF: *const [26]u8 = "\x00SOLANA_MERKLE_SHREDS_LEAF"; +const MERKLE_HASH_PREFIX_NODE: *const [26]u8 = "\x01SOLANA_MERKLE_SHREDS_NODE"; + +fn joinNodes(lhs: []const u8, rhs: []const u8) Hash { + // TODO check + return hashv(&.{ + MERKLE_HASH_PREFIX_NODE, + lhs[0..merkle_proof_entry_size], + rhs[0..merkle_proof_entry_size], + }); +} + +pub fn hashv(vals: []const []const u8) Hash { + var hasher = std.crypto.hash.sha2.Sha256.init(.{}); + for (vals) |val| hasher.update(val); + return .{ .data = hasher.finalResult() }; +} + +/// Where the merkle proof starts in the shred binary. +fn proofOffset(constants: ShredConstants, variant: ShredVariant) !usize { + return constants.headers_size + + try capacity(constants, variant) + + if (variant.chained) SIZE_OF_MERKLE_ROOT else 0; +} + +fn capacity(constants: ShredConstants, variant: ShredVariant) !usize { + std.debug.assert(variant.chained or !variant.resigned); + return checkedSub( + constants.payload_size, + constants.headers_size + + if (variant.chained) SIZE_OF_MERKLE_ROOT else 0 + + variant.proof_size * merkle_proof_entry_size + + if (variant.resigned) SIGNATURE_LENGTH else 0, + ) catch error.InvalidProofSize; +} + +/// Shred index in the erasure batch. +/// This only works for coding shreds. +fn codeIndex(shred: []const u8) ?usize { + const num_data_shreds: usize = @intCast(getInt(u16, shred, 83) orelse return null); + const position: usize = @intCast(getInt(u16, shred, 87) orelse return null); + return checkedAdd(num_data_shreds, position) catch null; +} + +/// Shred index in the erasure batch +/// This only works for data shreds. +fn dataIndex(shred: []const u8) ?usize { + const fec_set_index = getInt(u32, shred, 79) orelse return null; + const layout_index = shred_layout.getIndex(shred) orelse return null; + const index = checkedAdd(layout_index, fec_set_index) catch return null; + return @intCast(index); +} + const MerkleProofEntry = [merkle_proof_entry_size]u8; const merkle_proof_entry_size: usize = 20; +const MerkleProofIterator = Iterator(MerkleProofEntryList, MerkleProofEntry); + +pub fn Iterator(comptime Collection: type, comptime Item: type) type { + return struct { + list: Collection, + index: usize, + + pub fn next(self: *@This()) ?Item { + if (self.index >= self.list.len) { + return null; + } + return self.list.get(self.index); + } + }; +} + /// This is a reference. It does not own the data. Be careful with its lifetime. const MerkleProofEntryList = struct { bytes: []const u8, len: usize, - pub fn get(self: *@This(), index: usize) error{IndexOutOfBounds}!MerkleProofEntry { - if (index > self.len) return error.IndexOutOfBounds; + pub fn get(self: *@This(), index: usize) ?MerkleProofEntry { + if (index > self.len) return null; const start = index * merkle_proof_entry_size; const end = start + merkle_proof_entry_size; - return self.bytes[start..end]; + var entry: MerkleProofEntry = undefined; + @memcpy(&entry, self.bytes[start..end]); + return entry; + } + + pub fn iterator(self: @This()) MerkleProofIterator { + return .{ .list = self, .index = 0 }; } }; @@ -481,9 +578,11 @@ pub const shred_layout = struct { pub fn getSignedData(shred: []const u8) ?Hash { const variant = getShredVariant(shred) orelse return null; - _ = variant; - // TODO implement this once the leader schedule is available to runShredSigVerify - return Hash.default(); + const constants = switch (variant.shred_type) { + .Code => coding_shred, + .Data => data_shred, + }; + return getMerkleRoot(shred, constants, variant) catch null; } /// must be a data shred, otherwise the return value will be corrupted and meaningless @@ -491,21 +590,21 @@ pub const shred_layout = struct { std.debug.assert(getShredVariant(shred).?.shred_type == .Data); return getInt(u16, shred, 83); } - - /// Extracts a little-endian integer from within the slice, - /// starting at start_index. - fn getInt( - comptime Int: type, - data: []const u8, - start_index: usize, - ) ?Int { - const end_index = start_index + @sizeOf(Int); - if (data.len < end_index) return null; - const bytes: *const [@sizeOf(Int)]u8 = @ptrCast(data[start_index..end_index]); - return std.mem.readInt(Int, bytes, .little); - } }; +/// Extracts a little-endian integer from within the slice, +/// starting at start_index. +fn getInt( + comptime Int: type, + data: []const u8, + start_index: usize, +) ?Int { + const end_index = start_index + @sizeOf(Int); + if (data.len < end_index) return null; + const bytes: *const [@sizeOf(Int)]u8 = @ptrCast(data[start_index..end_index]); + return std.mem.readInt(Int, bytes, .little); +} + test "basic shred variant round trip" { try testShredVariantRoundTrip(0x4C, .{ .shred_type = .Code, diff --git a/src/trace/log.zig b/src/trace/log.zig index 08d3a2873..77eb09fea 100644 --- a/src/trace/log.zig +++ b/src/trace/log.zig @@ -92,6 +92,15 @@ pub const Logger = union(enum) { } } + pub fn logf(self: Self, level: Level, comptime fmt: []const u8, args: anytype) void { + switch (self) { + .standard => |logger| { + logger.logf(level, fmt, args); + }, + .noop => {}, + } + } + pub fn info(self: Self, msg: []const u8) void { switch (self) { .standard => |logger| { @@ -128,6 +137,15 @@ pub const Logger = union(enum) { } } + pub fn log(self: Self, level: Level, msg: []const u8) void { + switch (self) { + .standard => |logger| { + logger.log(level, msg); + }, + .noop => {}, + } + } + /// Can be used in tests to minimize the amount of logging during tests. pub const TEST_DEFAULT_LEVEL: Level = .warn; }; diff --git a/src/utils/service.zig b/src/utils/service.zig index cc9724620..3218f8ad6 100644 --- a/src/utils/service.zig +++ b/src/utils/service.zig @@ -7,8 +7,9 @@ const ArenaAllocator = std.heap.ArenaAllocator; const ArrayList = std.ArrayList; const Atomic = std.atomic.Value; -const Logger = sig.trace.Logger; const Lazy = sig.utils.Lazy; +const Level = sig.trace.Level; +const Logger = sig.trace.Logger; /// High level manager for long-running threads and the state /// shared by those threads. @@ -161,29 +162,42 @@ pub fn runService( var num_errors: u64 = 0; while (!exit.load(.unordered)) { const result = @call(.auto, function, args); + + // identify result if (result) |_| num_oks += 1 else |_| num_errors += 1; - const handler, const num_events, const event_name, const err: ?anyerror = if (result) |_| - .{ config.return_handler, num_oks, "return", null } - else |err| - .{ config.error_handler, num_errors, "error", err }; - if (handler.log_return) logger.errf("{s} has {s}ed: {}", .{ name, event_name, err orelse error.no_error }); + const handler, const num_events, const event_name, const level = if (result) |_| + .{ config.return_handler, num_oks, "return", Level.info } + else |_| + .{ config.error_handler, num_errors, "error", Level.err }; + + // handle result + if (handler.log_return) { + logger.logf(level, "{s} has {s}ed: {any}", .{ name, event_name, result }); + } if (handler.max_iterations) |max| if (num_events >= max) { if (handler.set_exit_on_completion) { - if (handler.log_exit) logger.errf("Signaling exit due to {}th {s} from {s}", .{ num_events, event_name, name }); + if (handler.log_exit) logger.logf( + level, + "Signaling exit due to {} {s}s from {s}", + .{ num_events, event_name, name }, + ); exit.store(true, .monotonic); - } else { - if (handler.log_exit) logger.errf("Exiting {s} due to {}th {s}", .{ name, num_events, event_name }); - } + } else if (handler.log_exit) logger.logf( + level, + "Exiting {s} due to {} {s}s", + .{ name, num_events, event_name }, + ); return result; }; + // sleep before looping, if necessary last_iteration = timer.lap(); std.time.sleep(@max( config.min_pause_ns, config.min_loop_duration_ns -| last_iteration, )); } - logger.infof("Exiting {s} due to exit signal received", .{name}); + logger.infof("Exiting {s} because the exit signal was received.", .{name}); } /// Defer actions until later. From 2dcc0df0684fb1d676f5209da4a349fbda05e9df Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 4 Jun 2024 10:46:52 -0400 Subject: [PATCH 12/28] refactor(gossip): remove RunHandles, superceded by ServiceManager --- src/gossip/service.zig | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/gossip/service.zig b/src/gossip/service.zig index 833442d5e..5a142ae12 100644 --- a/src/gossip/service.zig +++ b/src/gossip/service.zig @@ -263,30 +263,6 @@ pub const GossipService = struct { deinitMux(&self.failed_pull_hashes_mux); } - pub const RunHandles = struct { - exit: *AtomicBool, - receiver_thread: std.Thread, - packet_verifier_thread: std.Thread, - message_processor_thread: std.Thread, - message_builder_thread: ?std.Thread, - responder_thread: std.Thread, - dumper_thread: ?std.Thread, - - /// If any of the threads join, all other threads will be signalled to join. - pub fn joinAndExit(handles: RunHandles) void { - inline for (@typeInfo(RunHandles).Struct.fields, 0..) |field, i| cont: { - comptime if (@field(std.meta.FieldEnum(RunHandles), field.name) == .exit) { - std.debug.assert(field.type == *AtomicBool); - continue; - }; - const maybe_thread: ?std.Thread = @field(handles, field.name); - const thread = maybe_thread orelse break :cont; - thread.join(); // if we end up joining, something's gone wrong, so signal exit - if (i == 0) handles.exit.store(true, .unordered); - } - } - }; - pub const RunThreadsParams = struct { /// Allocator used to allocate message metadata. /// Helpful to use a dedicated allocator to reduce contention From b4e0753b8ad6a34651ed0f9e37a1e423e32da037 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Thu, 6 Jun 2024 09:31:05 -0400 Subject: [PATCH 13/28] fix(core, accountsdb): compute staked nodes and use for leader schedule --- src/accountsdb/snapshots.zig | 50 +++++++++++++++++++++++++++++++++++- src/core/leader_schedule.zig | 43 +++++++++++++++---------------- 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/src/accountsdb/snapshots.zig b/src/accountsdb/snapshots.zig index cdea044e0..fc6951b4f 100644 --- a/src/accountsdb/snapshots.zig +++ b/src/accountsdb/snapshots.zig @@ -52,7 +52,7 @@ pub const Stakes = struct { }; pub const VoteAccounts = struct { - vote_accounts: HashMap(Pubkey, struct { u64, Account }), + vote_accounts: HashMap(Pubkey, struct { u64, VoteAccount }), staked_nodes: ?HashMap( Pubkey, // VoteAccount.vote_state.node_pubkey. @@ -60,8 +60,56 @@ pub const VoteAccounts = struct { ) = null, pub const @"!bincode-config:staked_nodes" = bincode.FieldConfig(?HashMap(Pubkey, u64)){ .skip = true }; + + pub fn stakedNodes(self: *@This(), allocator: std.mem.Allocator) !*const HashMap(Pubkey, u64) { + const vote_accounts = self.vote_accounts; + var staked_nodes = HashMap(Pubkey, u64).init(allocator); + var iter = vote_accounts.iterator(); + while (iter.next()) |vote_entry| { + const vote_state = try vote_entry.value_ptr[1].voteState(); + const node_entry = try staked_nodes.getOrPut(vote_state.node_pubkey); + if (!node_entry.found_existing) { + node_entry.value_ptr.* = 0; + } + node_entry.value_ptr.* += vote_entry.value_ptr[0]; + } + self.staked_nodes = staked_nodes; + return &self.staked_nodes.?; + } +}; + +pub const VoteAccount = struct { + account: Account, + vote_state: ?anyerror!VoteState = null, + + pub const @"!bincode-config:vote_state" = bincode.FieldConfig(?anyerror!VoteState){ .skip = true }; + + pub fn voteState(self: *@This()) !VoteState { + if (self.vote_state) |vs| { + return vs; + } + self.vote_state = bincode.readFromSlice(undefined, VoteState, self.account.data, .{}); + return self.vote_state.?; + } }; +pub const VoteState = struct { + /// The variant of the rust enum + tag: u32, + /// the node that votes in this account + node_pubkey: Pubkey, +}; + +test "deserialize VoteState.node_pubkey" { + const bytes = .{ + 2, 0, 0, 0, 60, 155, 13, 144, 187, 252, 153, 72, 190, 35, 87, 94, 7, 178, + 90, 174, 158, 6, 199, 179, 134, 194, 112, 248, 166, 232, 144, 253, 128, 249, 67, 118, + } ++ .{0} ** 1586 ++ .{ 31, 0, 0, 0, 0, 0, 0, 0, 1 } ++ .{0} ** 24; + const vote_state = try bincode.readFromSlice(undefined, VoteState, &bytes, .{}); + const expected_pubkey = try Pubkey.fromString("55abJrqFnjm7ZRB1noVdh7BzBe3bBSMFT3pt16mw6Vad"); + try std.testing.expect(expected_pubkey.equals(&vote_state.node_pubkey)); +} + pub const Delegation = struct { /// to whom the stake is delegated voter_pubkey: Pubkey, diff --git a/src/core/leader_schedule.zig b/src/core/leader_schedule.zig index 33f441029..05f4b7064 100644 --- a/src/core/leader_schedule.zig +++ b/src/core/leader_schedule.zig @@ -37,18 +37,7 @@ pub fn leaderScheduleFromBank(allocator: Allocator, bank: *const Bank) ![]Pubkey const epoch = bank.bank_fields.epoch; const epoch_stakes = bank.bank_fields.epoch_stakes.getPtr(epoch) orelse return error.NoEpochStakes; const slots_in_epoch = bank.bank_fields.epoch_schedule.getSlotsInEpoch(epoch); - - const vote_accounts = epoch_stakes.stakes.vote_accounts.vote_accounts; - const staked_nodes = try allocator.alloc(StakedNode, vote_accounts.count()); - defer allocator.free(staked_nodes); - var iter = vote_accounts.iterator(); - var index: usize = 0; - while (iter.next()) |entry| : (index += 1) { - staked_nodes[index] = .{ - .id = entry.key_ptr.*, - .stake = entry.value_ptr.*[0], - }; - } + const staked_nodes = try epoch_stakes.stakes.vote_accounts.stakedNodes(allocator); return try leaderSchedule(allocator, staked_nodes, slots_in_epoch, epoch); } @@ -57,16 +46,25 @@ pub const StakedNode = struct { id: Pubkey, stake: u64 }; pub fn leaderSchedule( allocator: Allocator, - nodes: []StakedNode, + staked_nodes: *const std.AutoHashMap(Pubkey, u64), slots_in_epoch: Slot, epoch: Epoch, ) Allocator.Error![]Pubkey { - std.mem.sortUnstable(StakedNode, nodes, {}, struct { - fn gt(_: void, lhs: StakedNode, rhs: StakedNode) bool { - return switch (std.math.order(lhs.stake, rhs.stake)) { + const Entry = std.AutoHashMap(Pubkey, u64).Entry; + + const nodes = try allocator.alloc(Entry, staked_nodes.count()); + defer allocator.free(nodes); + var iter = staked_nodes.iterator(); + var index: usize = 0; + while (iter.next()) |staked_node_entry| : (index += 1) { + nodes[index] = staked_node_entry; + } + std.mem.sortUnstable(Entry, nodes, {}, struct { + fn gt(_: void, lhs: Entry, rhs: Entry) bool { + return switch (std.math.order(lhs.value_ptr.*, rhs.value_ptr.*)) { .gt => true, .lt => false, - .eq => .gt == std.mem.order(u8, &lhs.id.data, &rhs.id.data), + .eq => .gt == std.mem.order(u8, &lhs.key_ptr.data, &rhs.key_ptr.data), }; } }.gt); @@ -80,7 +78,7 @@ pub fn leaderSchedule( // init sampler from stake weights const stakes = try allocator.alloc(u64, nodes.len); defer allocator.free(stakes); - for (nodes, 0..) |entry, i| stakes[i] = entry.stake; + for (nodes, 0..) |entry, i| stakes[i] = entry.value_ptr.*; var sampler = try WeightedRandomSampler(u64).init(allocator, random, stakes); defer sampler.deinit(); @@ -89,7 +87,7 @@ pub fn leaderSchedule( var current_node: Pubkey = undefined; for (0..slots_in_epoch) |i| { if (i % NUM_CONSECUTIVE_LEADER_SLOTS == 0) { - current_node = nodes[sampler.sample()].id; + current_node = nodes[sampler.sample()].key_ptr.*; } slot_leaders[i] = current_node; } @@ -101,12 +99,13 @@ test "leaderSchedule calculation matches agave" { var rng = ChaChaRng(20).fromSeed(.{0} ** 32); const random = rng.random(); var pubkey_bytes: [32]u8 = undefined; - var staked_nodes: [100]StakedNode = undefined; - for (0..100) |i| { + var staked_nodes = std.AutoHashMap(Pubkey, u64).init(std.testing.allocator); + defer staked_nodes.deinit(); + for (0..100) |_| { random.bytes(&pubkey_bytes); const key = Pubkey{ .data = pubkey_bytes }; const stake = random.int(u64) / 1000; - staked_nodes[i] = .{ .id = key, .stake = stake }; + try staked_nodes.put(key, stake); } const slot_leaders = try leaderSchedule(std.testing.allocator, &staked_nodes, 321, 123); defer std.testing.allocator.free(slot_leaders); From 7789370ccf032773e91ea70691183d86d46532fe Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Mon, 10 Jun 2024 19:59:26 -0400 Subject: [PATCH 14/28] feat(cmd): add leader-schedule command (and refactor for modularity) --- src/accountsdb/lib.zig | 1 + src/cmd/cmd.zig | 425 +++++++++++++++++++++++++++-------------- src/gossip/service.zig | 16 +- 3 files changed, 291 insertions(+), 151 deletions(-) diff --git a/src/accountsdb/lib.zig b/src/accountsdb/lib.zig index e5e50f3a3..bc98e99e8 100644 --- a/src/accountsdb/lib.zig +++ b/src/accountsdb/lib.zig @@ -14,6 +14,7 @@ pub const AccountsDBConfig = _private.db.AccountsDBConfig; pub const AllSnapshotFields = _private.snapshots.AllSnapshotFields; pub const Bank = _private.bank.Bank; pub const GenesisConfig = _private.genesis_config.GenesisConfig; +pub const SnapshotFields = _private.snapshots.SnapshotFields; pub const SnapshotFieldsAndPaths = _private.snapshots.SnapshotFieldsAndPaths; pub const SnapshotFiles = _private.snapshots.SnapshotFiles; pub const StatusCache = _private.snapshots.StatusCache; diff --git a/src/cmd/cmd.zig b/src/cmd/cmd.zig index 2b12a0919..3621b4216 100644 --- a/src/cmd/cmd.zig +++ b/src/cmd/cmd.zig @@ -7,6 +7,7 @@ const helpers = @import("helpers.zig"); const sig = @import("../lib.zig"); const config = @import("config.zig"); +const Allocator = std.mem.Allocator; const Atomic = std.atomic.Value; const KeyPair = std.crypto.sign.Ed25519.KeyPair; const Random = std.rand.Random; @@ -346,6 +347,42 @@ var app = &cli.App{ }, }, }, + &cli.Command{ + .name = "leader-schedule", + .description = .{ + .one_line = "Prints the leader schedule from the snapshot", + .detailed = + \\- Starts gossip + \\- acquires a snapshot if necessary + \\- loads accounts db from the snapshot + \\- calculates the leader schedule from the snaphot + \\- prints the leader schedule in the same format as `solana leader-schedule` + \\- exits + , + }, + .options = &.{ + // gossip + &gossip_host.option, + &gossip_port_option, + &gossip_entrypoints_option, + &gossip_spy_node_option, + &gossip_dump_option, + // accounts-db + &snapshot_dir_option, + &n_threads_snapshot_load_option, + &n_threads_snapshot_unpack_option, + &disk_index_path_option, + &force_unpack_snapshot_option, + &min_snapshot_download_speed_mb_option, + &force_new_snapshot_download_option, + &trusted_validators_option, + }, + .target = .{ + .action = .{ + .exec = printLeaderSchedule, + }, + }, + }, }, }, }, @@ -365,168 +402,118 @@ fn identity() !void { /// entrypoint to run only gossip fn gossip() !void { - var logger = try spawnLogger(); - defer logger.deinit(); - const metrics_thread = try spawnMetrics(logger); - defer metrics_thread.detach(); + var app_base = try AppBase.init(gpa_allocator); - var exit = std.atomic.Value(bool).init(false); - const my_keypair = try getOrInitIdentity(gpa_allocator, logger); - const entrypoints = try getEntrypoints(logger); - defer entrypoints.deinit(); - const my_data = try getMyDataFromIpEcho(logger, entrypoints.items); - - var gossip_service = try initGossip( - logger, - my_keypair, - &exit, - entrypoints.items, - my_data.shred_version, - my_data.ip, - &.{}, - ); + var gossip_service, var gossip_manager = try startGossip(gpa_allocator, &app_base, &.{}); defer gossip_service.deinit(); + defer gossip_manager.deinit(); - try runGossipWithConfigValues(&gossip_service); + gossip_manager.join(); } /// entrypoint to run a full solana validator fn validator() !void { - var logger = try spawnLogger(); - defer logger.deinit(); - const metrics_thread = try spawnMetrics(logger); - defer metrics_thread.detach(); - - var rand = std.rand.DefaultPrng.init(@bitCast(std.time.timestamp())); - var exit = std.atomic.Value(bool).init(false); - const my_keypair = try getOrInitIdentity(gpa_allocator, logger); - const entrypoints = try getEntrypoints(logger); - defer entrypoints.deinit(); - const ip_echo_data = try getMyDataFromIpEcho(logger, entrypoints.items); + var app_base = try AppBase.init(gpa_allocator); const repair_port: u16 = config.current.shred_collector.repair_port; const tvu_port: u16 = config.current.shred_collector.repair_port; - // gossip - var gossip_service = try initGossip( - logger, - my_keypair, - &exit, - entrypoints.items, - ip_echo_data.shred_version, // TODO atomic owned at top level? or owned by gossip is good? - ip_echo_data.ip, - &.{ - .{ .tag = socket_tag.REPAIR, .port = repair_port }, - .{ .tag = socket_tag.TVU, .port = tvu_port }, - }, - ); - defer gossip_service.deinit(); - var gossip_handle = try gossip_service.start(.{ - .spy_node = config.current.gossip.spy_node, - .dump = config.current.gossip.dump, + var gossip_service, var gossip_manager = try startGossip(gpa_allocator, &app_base, &.{ + .{ .tag = socket_tag.REPAIR, .port = repair_port }, + .{ .tag = socket_tag.TVU, .port = tvu_port }, }); - defer gossip_handle.deinit(); - - // accounts db - var snapshots = try getOrDownloadSnapshots( - gpa_allocator, - logger, - &gossip_service, - ); - defer snapshots.deinit(gpa_allocator); - - logger.infof("full snapshot: {s}", .{snapshots.full_path}); - if (snapshots.incremental_path) |inc_path| { - logger.infof("incremental snapshot: {s}", .{inc_path}); - } - - // cli parsing - const snapshot_dir_str = config.current.accounts_db.snapshot_dir; - const n_cpus = @as(u32, @truncate(try std.Thread.getCpuCount())); - var n_threads_snapshot_load: u32 = @intCast(config.current.accounts_db.num_threads_snapshot_load); - if (n_threads_snapshot_load == 0) { - n_threads_snapshot_load = n_cpus; - } - - var accounts_db = try AccountsDB.init( - gpa_allocator, - logger, - AccountsDBConfig{ - .disk_index_path = config.current.accounts_db.disk_index_path, - .storage_cache_size = @intCast(config.current.accounts_db.storage_cache_size), - .number_of_index_bins = @intCast(config.current.accounts_db.num_account_index_bins), - }, - ); - defer accounts_db.deinit(); - - const snapshot_fields = try accounts_db.loadWithDefaults( - &snapshots, - snapshot_dir_str, - n_threads_snapshot_load, - true, // validate too - ); - const bank_fields = snapshot_fields.bank_fields; - - // this should exist before we start to unpack - logger.infof("reading genesis...", .{}); - const genesis_config = readGenesisConfig(gpa_allocator, snapshot_dir_str) catch |err| { - if (err == error.GenesisNotFound) { - logger.errf("genesis.bin not found - expecting {s}/genesis.bin to exist", .{snapshot_dir_str}); - } - return err; - }; - defer genesis_config.deinit(gpa_allocator); - - logger.infof("validating bank...", .{}); - const bank = Bank.init(&accounts_db, &bank_fields); - try Bank.validateBankFields(bank.bank_fields, &genesis_config); - - // validate the status cache - logger.infof("validating status cache...", .{}); - var status_cache = readStatusCache(gpa_allocator, snapshot_dir_str) catch |err| { - if (err == error.StatusCacheNotFound) { - logger.errf("status-cache.bin not found - expecting {s}/snapshots/status-cache to exist", .{snapshot_dir_str}); - } - return err; - }; - defer status_cache.deinit(); + defer gossip_service.deinit(); + defer gossip_manager.deinit(); - var slot_history = try accounts_db.getSlotHistory(); - defer slot_history.deinit(accounts_db.allocator); - try status_cache.validate(gpa_allocator, bank_fields.slot, &slot_history); - - logger.infof("accounts-db setup done...", .{}); + const snapshot = try loadSnapshot(gpa_allocator, app_base.logger, gossip_service, false); // leader schedule - const leader_schedule = try sig.core.leader_schedule.leaderScheduleFromBank(gpa_allocator, &bank); - _, const slot_index = bank.bank_fields.epoch_schedule.getEpochAndSlotIndex(bank.bank_fields.slot); - const epoch_start_slot = bank.bank_fields.slot - slot_index; - var slot_leader_getter = sig.core.leader_schedule.SingleEpochLeaderSchedule{ - .leader_schedule = leader_schedule, - .start_slot = epoch_start_slot, - }; - const getter = slot_leader_getter.provider(); + var leader_schedule = try leaderSchedule(gpa_allocator, &snapshot.bank); + const leader_provider = leader_schedule.provider(); // shred collector + var rng = std.rand.DefaultPrng.init(@bitCast(std.time.timestamp())); var shred_collector = try sig.shred_collector.start( config.current.shred_collector, ShredCollectorDependencies{ .allocator = gpa_allocator, - .logger = logger, - .random = rand.random(), - .my_keypair = &my_keypair, - .exit = &exit, + .logger = app_base.logger, + .random = rng.random(), + .my_keypair = &app_base.my_keypair, + .exit = &app_base.exit, .gossip_table_rw = &gossip_service.gossip_table_rw, .my_shred_version = &gossip_service.my_shred_version, - .leader_schedule = getter, + .leader_schedule = leader_provider, }, ); defer shred_collector.deinit(); - gossip_handle.join(); + gossip_manager.join(); shred_collector.join(); } +/// entrypoint to print the leader schedule and then exit +fn printLeaderSchedule() !void { + const allocator = gpa_allocator; + var app_base = try AppBase.init(allocator); + + var gossip_service, var gossip_manager = try startGossip(allocator, &app_base, &.{}); + defer gossip_service.deinit(); + defer gossip_manager.deinit(); + + const snapshot = try loadSnapshot(allocator, app_base.logger, gossip_service, false); + const leader_schedule = try leaderSchedule(allocator, &snapshot.bank); + + // const buf = try allocator.alloc(u8, 64 * leader_schedule.leader_schedule.len); + // var stream = std.io.fixedBufferStream(buf); + // const stdout = std.io.getStdOut(); + var stdout = std.io.bufferedWriter(std.io.getStdOut().writer()); + const writer = stdout.writer(); + for (leader_schedule.leader_schedule, 0..) |leader, i| { + try writer.print(" {} {s}\n", .{ i + leader_schedule.start_slot, &leader.string() }); + } + // try std.io.getStdOut().writeAll(buf[0..stream.pos]); +} + +/// State that typically needs to be initialized at the start of the app, +/// and deinitialized only when the app exits. +const AppBase = struct { + exit: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), + logger: Logger, + metrics_thread: std.Thread, + my_keypair: KeyPair, + entrypoints: std.ArrayList(SocketAddr), + shred_version: u16, + my_ip: IpAddr, + + fn init(allocator: Allocator) !AppBase { + var logger = try spawnLogger(); + errdefer logger.deinit(); + const metrics_thread = try spawnMetrics(logger); + errdefer metrics_thread.detach(); + const my_keypair = try getOrInitIdentity(allocator, logger); + const entrypoints = try getEntrypoints(logger); + errdefer entrypoints.deinit(); + const ip_echo_data = try getMyDataFromIpEcho(logger, entrypoints.items); + + return .{ + .logger = logger, + .metrics_thread = metrics_thread, + .my_keypair = my_keypair, + .entrypoints = entrypoints, + .shred_version = ip_echo_data.shred_version, + .my_ip = ip_echo_data.ip, + }; + } + + pub fn deinit(self: @This()) void { + self.exit.store(true, .unordered); + self.entrypoints.deinit(); + self.metrics_thread.detach(); + self.logger.deinit(); + } +}; + /// Initialize an instance of GossipService and configure with CLI arguments fn initGossip( logger: Logger, @@ -559,6 +546,51 @@ fn initGossip( ); } +fn startGossip( + allocator: Allocator, + app_base: *AppBase, + /// Extra sockets to publish in gossip, other than the gossip socket + extra_sockets: []const struct { tag: u8, port: u16 }, +) !struct { *GossipService, sig.utils.service_manager.ServiceManager } { + const gossip_port = config.current.gossip.port; + app_base.logger.infof("gossip host: {any}", .{app_base.my_ip}); + app_base.logger.infof("gossip port: {d}", .{gossip_port}); + + // setup contact info + const my_pubkey = Pubkey.fromPublicKey(&app_base.my_keypair.public_key); + var contact_info = ContactInfo.init(allocator, my_pubkey, getWallclockMs(), 0); + try contact_info.setSocket(socket_tag.GOSSIP, SocketAddr.init(app_base.my_ip, gossip_port)); + for (extra_sockets) |s| try contact_info.setSocket(s.tag, SocketAddr.init(app_base.my_ip, s.port)); + contact_info.shred_version = app_base.shred_version; + + var manager = sig.utils.service_manager.ServiceManager.init( + allocator, + app_base.logger, + &app_base.exit, + "gossip", + .{}, + .{}, + ); + const service = try manager.arena().create(GossipService); + service.* = try GossipService.init( + gpa_allocator, + gossip_value_gpa_allocator, + contact_info, + app_base.my_keypair, // TODO: consider security implication of passing keypair by value + app_base.entrypoints.items, + &app_base.exit, + app_base.logger, + ); + try manager.defers.deferCall(GossipService.deinit, .{service}); + + try service.start(.{ + .spy_node = config.current.gossip.spy_node, + .dump = config.current.gossip.dump, + }, &manager); + + return .{ service, manager }; +} + fn runGossipWithConfigValues(gossip_service: *GossipService) !void { const gossip_config = config.current.gossip; return gossip_service.run(.{ @@ -667,11 +699,121 @@ fn spawnLogger() !Logger { return logger; } +fn leaderSchedule( + allocator: Allocator, + bank: *const Bank, +) !sig.core.leader_schedule.SingleEpochLeaderSchedule { + const leader_schedule = try sig.core.leader_schedule.leaderScheduleFromBank(allocator, bank); + _, const slot_index = bank.bank_fields.epoch_schedule.getEpochAndSlotIndex(bank.bank_fields.slot); + const epoch_start_slot = bank.bank_fields.slot - slot_index; + const schedule = sig.core.leader_schedule.SingleEpochLeaderSchedule{ + .leader_schedule = leader_schedule, + .start_slot = epoch_start_slot, + }; + return schedule; +} + +const LoadedSnapshot = struct { + allocator: Allocator, + accounts_db: AccountsDB, + snapshots: SnapshotFieldsAndPaths, + snapshot_fields: sig.accounts_db.SnapshotFields, + /// contains pointers to `accounts_db` and `snapshot_fields` + bank: Bank, + genesis_config: GenesisConfig, + + pub fn deinit(self: *@This()) void { + self.genesis_config.deinit(self.allocator); + self.snapshot_fields.deinit(self.allocator); + self.accounts_db.deinit(); + self.snapshots.deinit(self.allocator); + self.allocator.destroy(self); + } +}; + +fn loadSnapshot( + allocator: Allocator, + logger: Logger, + gossip_service: *GossipService, + validate: bool, +) !*LoadedSnapshot { + const output = try allocator.create(LoadedSnapshot); + var snapshots = try getOrDownloadSnapshots( + allocator, + logger, + gossip_service, + ); + + logger.infof("full snapshot: {s}", .{snapshots.full_path}); + if (snapshots.incremental_path) |inc_path| { + logger.infof("incremental snapshot: {s}", .{inc_path}); + } + + // cli parsing + const snapshot_dir_str = config.current.accounts_db.snapshot_dir; + const n_cpus = @as(u32, @truncate(try std.Thread.getCpuCount())); + var n_threads_snapshot_load: u32 = @intCast(config.current.accounts_db.num_threads_snapshot_load); + if (n_threads_snapshot_load == 0) { + n_threads_snapshot_load = n_cpus; + } + + output.accounts_db = try AccountsDB.init( + allocator, + logger, + AccountsDBConfig{ + .disk_index_path = config.current.accounts_db.disk_index_path, + .storage_cache_size = @intCast(config.current.accounts_db.storage_cache_size), + .number_of_index_bins = @intCast(config.current.accounts_db.num_account_index_bins), + }, + ); + + output.snapshot_fields = try output.accounts_db.loadWithDefaults( + &snapshots, + snapshot_dir_str, + n_threads_snapshot_load, + true, // validate too + ); + + const bank_fields = &output.snapshot_fields.bank_fields; + + // this should exist before we start to unpack + logger.infof("reading genesis...", .{}); + output.genesis_config = readGenesisConfig(allocator, snapshot_dir_str) catch |err| { + if (err == error.GenesisNotFound) { + logger.errf("genesis.bin not found - expecting {s}/genesis.bin to exist", .{snapshot_dir_str}); + } + return err; + }; + + logger.infof("validating bank...", .{}); + output.bank = Bank.init(&output.accounts_db, bank_fields); + + // TODO: remove this condition once these validations work reliably on all clusters + if (validate) { + try Bank.validateBankFields(output.bank.bank_fields, &output.genesis_config); + + // validate the status cache + logger.infof("validating status cache...", .{}); + var status_cache = readStatusCache(allocator, snapshot_dir_str) catch |err| { + if (err == error.StatusCacheNotFound) { + logger.errf("status-cache.bin not found - expecting {s}/snapshots/status-cache to exist", .{snapshot_dir_str}); + } + return err; + }; + defer status_cache.deinit(); + + var slot_history = try output.accounts_db.getSlotHistory(); + defer slot_history.deinit(output.accounts_db.allocator); + try status_cache.validate(allocator, bank_fields.slot, &slot_history); + } + + logger.infof("accounts-db setup done...", .{}); + + return output; +} + /// load genesis config with default filenames -fn readGenesisConfig( - allocator: std.mem.Allocator, - snapshot_dir: []const u8, -) !GenesisConfig { +fn readGenesisConfig(allocator: Allocator, snapshot_dir: []const u8) !GenesisConfig { const genesis_path = try std.fmt.allocPrint( allocator, "{s}/genesis.bin", @@ -687,10 +829,7 @@ fn readGenesisConfig( return genesis_config; } -fn readStatusCache( - allocator: std.mem.Allocator, - snapshot_dir: []const u8, -) !StatusCache { +fn readStatusCache(allocator: Allocator, snapshot_dir: []const u8) !StatusCache { const status_cache_path = try std.fmt.allocPrint( gpa_allocator, "{s}/{s}", @@ -746,9 +885,7 @@ fn downloadSnapshot() !void { ); } -fn getTrustedValidators( - allocator: std.mem.Allocator, -) !?std.ArrayList(Pubkey) { +fn getTrustedValidators(allocator: Allocator) !?std.ArrayList(Pubkey) { var trusted_validators: ?std.ArrayList(Pubkey) = null; if (config.current.gossip.trusted_validators.len > 0) { trusted_validators = try std.ArrayList(Pubkey).initCapacity( @@ -766,7 +903,7 @@ fn getTrustedValidators( } fn getOrDownloadSnapshots( - allocator: std.mem.Allocator, + allocator: Allocator, logger: Logger, gossip_service: ?*GossipService, ) !SnapshotFieldsAndPaths { diff --git a/src/gossip/service.zig b/src/gossip/service.zig index 8c9b93c09..f42dfbb82 100644 --- a/src/gossip/service.zig +++ b/src/gossip/service.zig @@ -277,9 +277,10 @@ pub const GossipService = struct { /// starts gossip and blocks until it exits pub fn run(self: *Self, params: RunThreadsParams) !void { - var service = try self.start(params); - service.join(); - service.deinit(); + var manager = ServiceManager.init(self.allocator, self.logger, self.exit, "gossip", .{}, .{}); + try self.start(params, &manager); + manager.join(); + manager.deinit(); } /// spawns required threads for the gossip service and returns immediately @@ -290,12 +291,15 @@ pub const GossipService = struct { /// 4) build message loop (to send outgoing message) (only active if not a spy node) /// 5) a socket responder (to send outgoing packets) /// 6) echo server - pub fn start(self: *Self, params: RunThreadsParams) std.Thread.SpawnError!ServiceManager { + pub fn start( + self: *Self, + params: RunThreadsParams, + manager: *ServiceManager, + ) (std.mem.Allocator.Error || std.Thread.SpawnError)!void { // TODO(Ahmad): need new server impl, for now we don't join server thread // because http.zig's server doesn't stop when you call server.stop() - it's broken // const echo_server_thread = try self.echo_server.listenAndServe(); // _ = echo_server_thread; - var manager = ServiceManager.init(self.allocator, self.logger, self.exit, "gossip", .{}, .{}); errdefer manager.deinit(); try manager.spawn("gossip readSocket", socket_utils.readSocket, .{ @@ -323,8 +327,6 @@ pub const GossipService = struct { .gossip_table_rw = &self.gossip_table_rw, .exit = self.exit, }}); - - return manager; } const VerifyMessageTask = ThreadPoolTask(VerifyMessageEntry); From 65af230aca536fc9299ceea828322052ec9949ce Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 11 Jun 2024 22:47:44 -0400 Subject: [PATCH 15/28] feat: parse leader schedule --- .gitignore | 1 + src/cmd/cmd.zig | 75 +++++++++++--------- src/cmd/config.zig | 1 + src/core/leader_schedule.zig | 130 ++++++++++++++++++++++++++++++++--- src/core/pubkey.zig | 2 +- 5 files changed, 166 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index 3e9273b43..6152ca1d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .zig-cache/ +zig-cache/ zig-out/ data/ diff --git a/src/cmd/cmd.zig b/src/cmd/cmd.zig index eb5401a95..e97dbe3c5 100644 --- a/src/cmd/cmd.zig +++ b/src/cmd/cmd.zig @@ -22,6 +22,7 @@ const Level = sig.trace.Level; const Logger = sig.trace.Logger; const Pubkey = sig.core.Pubkey; const ShredCollectorDependencies = sig.shred_collector.ShredCollectorDependencies; +const SingleEpochLeaderSchedule = sig.core.leader_schedule.SingleEpochLeaderSchedule; const SnapshotFieldsAndPaths = sig.accounts_db.SnapshotFieldsAndPaths; const SnapshotFiles = sig.accounts_db.SnapshotFiles; const SocketAddr = sig.net.SocketAddr; @@ -32,9 +33,12 @@ const enumFromName = sig.utils.types.enumFromName; const getOrInitIdentity = helpers.getOrInitIdentity; const globalRegistry = sig.prometheus.globalRegistry; const getWallclockMs = sig.gossip.getWallclockMs; +const leaderScheduleFromBank = sig.core.leader_schedule.leaderScheduleFromBank; const parallelUnpackZstdTarBall = sig.accounts_db.parallelUnpackZstdTarBall; +const parseLeaderSchedule = sig.core.leader_schedule.parseLeaderSchedule; const requestIpEcho = sig.net.requestIpEcho; const servePrometheus = sig.prometheus.servePrometheus; +const writeLeaderSchedule = sig.core.leader_schedule.writeLeaderSchedule; const socket_tag = sig.gossip.socket_tag; @@ -95,6 +99,14 @@ var turbine_recv_port_option = cli.Option{ .value_name = "Turbine Port", }; +var leader_schedule_option = cli.Option{ + .long_name = "leader-schedule", + .help = "Set a file path to load the leader schedule. Use '--' to load from stdin", + .value_ref = cli.mkRef(&config.current.leader_schedule_path), + .required = false, + .value_name = "Leader schedule source", +}; + var test_repair_option = cli.Option{ .long_name = "test-repair-for-slot", .help = "Set a slot here to repeatedly send repair requests for shreds from this slot. This is only intended for use during short-lived tests of the repair service. Do not set this during normal usage.", @@ -310,6 +322,8 @@ var app = &cli.App{ &min_snapshot_download_speed_mb_option, &force_new_snapshot_download_option, &trusted_validators_option, + // general + &leader_schedule_option, }, .target = .{ .action = .{ @@ -371,6 +385,8 @@ var app = &cli.App{ &min_snapshot_download_speed_mb_option, &force_new_snapshot_download_option, &trusted_validators_option, + // general + &leader_schedule_option, }, .target = .{ .action = .{ @@ -408,22 +424,24 @@ fn gossip() !void { /// entrypoint to run a full solana validator fn validator() !void { - var app_base = try AppBase.init(gpa_allocator); + const allocator = gpa_allocator; + var app_base = try AppBase.init(allocator); const repair_port: u16 = config.current.shred_collector.repair_port; const turbine_recv_port: u16 = config.current.shred_collector.repair_port; - var gossip_service, var gossip_manager = try startGossip(gpa_allocator, &app_base, &.{ + var gossip_service, var gossip_manager = try startGossip(allocator, &app_base, &.{ .{ .tag = socket_tag.REPAIR, .port = repair_port }, .{ .tag = socket_tag.TURBINE_RECV, .port = turbine_recv_port }, }); defer gossip_service.deinit(); defer gossip_manager.deinit(); - const snapshot = try loadSnapshot(gpa_allocator, app_base.logger, gossip_service, false); + const snapshot = try loadSnapshot(allocator, app_base.logger, gossip_service, false); // leader schedule - var leader_schedule = try leaderSchedule(gpa_allocator, &snapshot.bank); + var leader_schedule = try getLeaderScheduleFromCli(allocator) orelse + try leaderScheduleFromBank(allocator, &snapshot.bank); const leader_provider = leader_schedule.provider(); // shred collector @@ -431,7 +449,7 @@ fn validator() !void { var shred_collector = try sig.shred_collector.start( config.current.shred_collector, ShredCollectorDependencies{ - .allocator = gpa_allocator, + .allocator = allocator, .logger = app_base.logger, .random = rng.random(), .my_keypair = &app_base.my_keypair, @@ -452,22 +470,27 @@ fn printLeaderSchedule() !void { const allocator = gpa_allocator; var app_base = try AppBase.init(allocator); - var gossip_service, var gossip_manager = try startGossip(allocator, &app_base, &.{}); - defer gossip_service.deinit(); - defer gossip_manager.deinit(); - - const snapshot = try loadSnapshot(allocator, app_base.logger, gossip_service, false); - const leader_schedule = try leaderSchedule(allocator, &snapshot.bank); + const leader_schedule = try getLeaderScheduleFromCli(allocator) orelse b: { + var gossip_service, var gossip_manager = try startGossip(allocator, &app_base, &.{}); + defer gossip_service.deinit(); + defer gossip_manager.deinit(); + const snapshot = try loadSnapshot(allocator, app_base.logger, gossip_service, false); + break :b try leaderScheduleFromBank(allocator, &snapshot.bank); + }; - // const buf = try allocator.alloc(u8, 64 * leader_schedule.leader_schedule.len); - // var stream = std.io.fixedBufferStream(buf); - // const stdout = std.io.getStdOut(); var stdout = std.io.bufferedWriter(std.io.getStdOut().writer()); - const writer = stdout.writer(); - for (leader_schedule.leader_schedule, 0..) |leader, i| { - try writer.print(" {} {s}\n", .{ i + leader_schedule.start_slot, &leader.string() }); - } - // try std.io.getStdOut().writeAll(buf[0..stream.pos]); + try writeLeaderSchedule(leader_schedule, stdout.writer()); + try stdout.flush(); +} + +fn getLeaderScheduleFromCli(allocator: Allocator) !?SingleEpochLeaderSchedule { + return if (config.current.leader_schedule_path) |path| + if (std.mem.eql(u8, "--", path)) + try parseLeaderSchedule(allocator, std.io.getStdIn().reader()) + else + try parseLeaderSchedule(allocator, (try std.fs.cwd().openFile(path, .{})).reader()) + else + null; } /// State that typically needs to be initialized at the start of the app, @@ -697,20 +720,6 @@ fn spawnLogger() !Logger { return logger; } -fn leaderSchedule( - allocator: Allocator, - bank: *const Bank, -) !sig.core.leader_schedule.SingleEpochLeaderSchedule { - const leader_schedule = try sig.core.leader_schedule.leaderScheduleFromBank(allocator, bank); - _, const slot_index = bank.bank_fields.epoch_schedule.getEpochAndSlotIndex(bank.bank_fields.slot); - const epoch_start_slot = bank.bank_fields.slot - slot_index; - const schedule = sig.core.leader_schedule.SingleEpochLeaderSchedule{ - .leader_schedule = leader_schedule, - .start_slot = epoch_start_slot, - }; - return schedule; -} - const LoadedSnapshot = struct { allocator: Allocator, accounts_db: AccountsDB, diff --git a/src/cmd/config.zig b/src/cmd/config.zig index 67f06c6ea..34fcef890 100644 --- a/src/cmd/config.zig +++ b/src/cmd/config.zig @@ -9,6 +9,7 @@ pub const Config = struct { // general config log_level: []const u8 = "debug", metrics_port: u16 = 12345, + leader_schedule_path: ?[]const u8 = null, }; pub const current: *Config = &default_validator_config; diff --git a/src/core/leader_schedule.zig b/src/core/leader_schedule.zig index 05f4b7064..cc22c3452 100644 --- a/src/core/leader_schedule.zig +++ b/src/core/leader_schedule.zig @@ -6,7 +6,7 @@ const Allocator = std.mem.Allocator; const Bank = sig.accounts_db.Bank; const ChaChaRng = sig.rand.ChaChaRng; const Epoch = sig.core.Epoch; -const EpochStakes = sig.accounts_db.EpochStakes; +const EpochStakes = sig.accounts_db.snapshots.EpochStakes; const Pubkey = sig.core.Pubkey; const Slot = sig.core.Slot; const WeightedRandomSampler = sig.rand.WeightedRandomSampler; @@ -18,14 +18,19 @@ pub const SlotLeaderProvider = sig.utils.closure.PointerClosure(Slot, ?Pubkey); /// Only works for a single epoch. This is a basic limited approach that should /// only be used as a placeholder until a better approach is fleshed out. pub const SingleEpochLeaderSchedule = struct { - leader_schedule: []const sig.core.Pubkey, - start_slot: sig.core.Slot, + allocator: Allocator, + slot_leaders: []const sig.core.Pubkey, + start_slot: Slot, const Self = @This(); - pub fn getLeader(self: *const Self, slot: sig.core.Slot) ?sig.core.Pubkey { + pub fn deinit(self: Self) void { + self.allocator.free(self.slot_leaders); + } + + pub fn getLeader(self: *const Self, slot: Slot) ?sig.core.Pubkey { const index: usize = @intCast(slot - self.start_slot); - return if (index >= self.leader_schedule.len) null else self.leader_schedule[index]; + return if (index >= self.slot_leaders.len) null else self.slot_leaders[index]; } pub fn provider(self: *Self) SlotLeaderProvider { @@ -33,13 +38,21 @@ pub const SingleEpochLeaderSchedule = struct { } }; -pub fn leaderScheduleFromBank(allocator: Allocator, bank: *const Bank) ![]Pubkey { +pub fn leaderScheduleFromBank(allocator: Allocator, bank: *const Bank) !SingleEpochLeaderSchedule { const epoch = bank.bank_fields.epoch; const epoch_stakes = bank.bank_fields.epoch_stakes.getPtr(epoch) orelse return error.NoEpochStakes; const slots_in_epoch = bank.bank_fields.epoch_schedule.getSlotsInEpoch(epoch); const staked_nodes = try epoch_stakes.stakes.vote_accounts.stakedNodes(allocator); - return try leaderSchedule(allocator, staked_nodes, slots_in_epoch, epoch); + const slot_leaders = try leaderSchedule(allocator, staked_nodes, slots_in_epoch, epoch); + + _, const slot_index = bank.bank_fields.epoch_schedule.getEpochAndSlotIndex(bank.bank_fields.slot); + const epoch_start_slot = bank.bank_fields.slot - slot_index; + return SingleEpochLeaderSchedule{ + .allocator = allocator, + .slot_leaders = slot_leaders, + .start_slot = epoch_start_slot, + }; } pub const StakedNode = struct { id: Pubkey, stake: u64 }; @@ -95,6 +108,56 @@ pub fn leaderSchedule( return slot_leaders; } +pub fn writeLeaderSchedule(sched: SingleEpochLeaderSchedule, writer: anytype) !void { + for (sched.slot_leaders, 0..) |leader, i| { + try writer.print(" {} {s}\n", .{ i + sched.start_slot, &leader.string() }); + } +} + +/// Parses the leader schedule as formatted by the `solana leader-schedule` and +/// `sig leader-schedule` commands. +pub fn parseLeaderSchedule( + allocator: std.mem.Allocator, + reader: anytype, +) !SingleEpochLeaderSchedule { + var slot_leaders = std.ArrayList(Pubkey).init(allocator); + var start_slot: Slot = 0; + var expect: ?Slot = null; + var row: [256]u8 = undefined; + while (true) { + const line = reader.readUntilDelimiter(&row, '\n') catch |e| switch (e) { + error.EndOfStream => break, + else => return e, + }; + var word_iter = std.mem.split(u8, line, " "); + const slot = try std.fmt.parseInt(Slot, nextNonEmpty(&word_iter) orelse continue, 10); + if (expect) |*exp_slot| { + if (slot != exp_slot.*) { + return error.Discontinuity; + } + exp_slot.* += 1; + } else { + expect = slot + 1; + start_slot = slot; + } + const node_str = nextNonEmpty(&word_iter) orelse return error.MissingPubkey; + try slot_leaders.append(try Pubkey.fromString(node_str)); + } + return .{ + .allocator = allocator, + .slot_leaders = try allocator.realloc( + slot_leaders.items.ptr[0..slot_leaders.capacity], + slot_leaders.items.len, + ), + .start_slot = start_slot, + }; +} + +fn nextNonEmpty(word_iter: anytype) ?[]const u8 { + while (word_iter.next()) |word| if (word.len > 0) return word; + return null; +} + test "leaderSchedule calculation matches agave" { var rng = ChaChaRng(20).fromSeed(.{0} ** 32); const random = rng.random(); @@ -110,11 +173,60 @@ test "leaderSchedule calculation matches agave" { const slot_leaders = try leaderSchedule(std.testing.allocator, &staked_nodes, 321, 123); defer std.testing.allocator.free(slot_leaders); for (slot_leaders, 0..) |slot_leader, i| { - try std.testing.expect((try Pubkey.fromString(expected[i])).equals(&slot_leader)); + try std.testing.expect((try Pubkey.fromString(generated_leader_schedule[i])).equals(&slot_leader)); } } -const expected = [_][]const u8{ +test "parseLeaderSchedule writeLeaderSchedule happy path roundtrip" { + const allocator = std.testing.allocator; + const input_file = + \\ 270864000 Fd7btgySsrjuo25CJCj7oE7VPMyezDhnx7pZkj2v69Nk + \\ 270864001 Fd7btgySsrjuo25CJCj7oE7VPMyezDhnx7pZkj2v69Nk + \\ 270864002 Fd7btgySsrjuo25CJCj7oE7VPMyezDhnx7pZkj2v69Nk + \\ 270864003 Fd7btgySsrjuo25CJCj7oE7VPMyezDhnx7pZkj2v69Nk + \\ 270864004 GBuP6xK2zcUHbQuUWM4gbBjom46AomsG8JzSp1bzJyn8 + \\ 270864005 GBuP6xK2zcUHbQuUWM4gbBjom46AomsG8JzSp1bzJyn8 + \\ 270864006 GBuP6xK2zcUHbQuUWM4gbBjom46AomsG8JzSp1bzJyn8 + \\ 270864007 GBuP6xK2zcUHbQuUWM4gbBjom46AomsG8JzSp1bzJyn8 + \\ 270864008 DWvDTSh3qfn88UoQTEKRV2JnLt5jtJAVoiCo3ivtMwXP + \\ 270864009 DWvDTSh3qfn88UoQTEKRV2JnLt5jtJAVoiCo3ivtMwXP + \\ 270864010 DWvDTSh3qfn88UoQTEKRV2JnLt5jtJAVoiCo3ivtMwXP + \\ + ; + const expected_nodes = [_]Pubkey{ + try Pubkey.fromString("Fd7btgySsrjuo25CJCj7oE7VPMyezDhnx7pZkj2v69Nk"), + try Pubkey.fromString("Fd7btgySsrjuo25CJCj7oE7VPMyezDhnx7pZkj2v69Nk"), + try Pubkey.fromString("Fd7btgySsrjuo25CJCj7oE7VPMyezDhnx7pZkj2v69Nk"), + try Pubkey.fromString("Fd7btgySsrjuo25CJCj7oE7VPMyezDhnx7pZkj2v69Nk"), + try Pubkey.fromString("GBuP6xK2zcUHbQuUWM4gbBjom46AomsG8JzSp1bzJyn8"), + try Pubkey.fromString("GBuP6xK2zcUHbQuUWM4gbBjom46AomsG8JzSp1bzJyn8"), + try Pubkey.fromString("GBuP6xK2zcUHbQuUWM4gbBjom46AomsG8JzSp1bzJyn8"), + try Pubkey.fromString("GBuP6xK2zcUHbQuUWM4gbBjom46AomsG8JzSp1bzJyn8"), + try Pubkey.fromString("DWvDTSh3qfn88UoQTEKRV2JnLt5jtJAVoiCo3ivtMwXP"), + try Pubkey.fromString("DWvDTSh3qfn88UoQTEKRV2JnLt5jtJAVoiCo3ivtMwXP"), + try Pubkey.fromString("DWvDTSh3qfn88UoQTEKRV2JnLt5jtJAVoiCo3ivtMwXP"), + }; + const expected_start = 270864000; + + // parse input file + var stream = std.io.fixedBufferStream(input_file); + const leader_schedule = try parseLeaderSchedule(allocator, stream.reader()); + defer leader_schedule.deinit(); + try std.testing.expect(expected_start == leader_schedule.start_slot); + try std.testing.expect(expected_nodes.len == leader_schedule.slot_leaders.len); + for (expected_nodes, leader_schedule.slot_leaders) |expected, actual| { + try std.testing.expect(expected.equals(&actual)); + } + + // write file out + var out_buf: [2 * input_file.len]u8 = undefined; + var out_stream = std.io.fixedBufferStream(&out_buf); + try writeLeaderSchedule(leader_schedule, out_stream.writer()); + const out_file = out_stream.getWritten(); + try std.testing.expect(std.mem.eql(u8, out_file, input_file)); +} + +const generated_leader_schedule = [_][]const u8{ "HU1g6zZ3LrXJeFYEmDAekv44kAv9XE8g8FQYz3rSrmNY", "HU1g6zZ3LrXJeFYEmDAekv44kAv9XE8g8FQYz3rSrmNY", "HU1g6zZ3LrXJeFYEmDAekv44kAv9XE8g8FQYz3rSrmNY", "HU1g6zZ3LrXJeFYEmDAekv44kAv9XE8g8FQYz3rSrmNY", "AvsmCG8R1qGJtRvjqudkX974ihfbYZUVf4t515tzxyHv", "AvsmCG8R1qGJtRvjqudkX974ihfbYZUVf4t515tzxyHv", diff --git a/src/core/pubkey.zig b/src/core/pubkey.zig index 7c9310cc3..420cdd396 100644 --- a/src/core/pubkey.zig +++ b/src/core/pubkey.zig @@ -16,7 +16,7 @@ pub const Pubkey = extern struct { pub fn fromString(str: []const u8) !Self { var out: [32]u8 = undefined; const written = decoder.decode(str, &out) catch return Error.InvalidEncodedValue; - if (written != 32) @panic("written is not 32"); + if (written != 32) return Error.InvalidBytesLength; return Self{ .data = out }; } From 492247aa16d6c19e7f2f1d42bd0eb54eccac0274 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 12 Jun 2024 09:39:06 -0400 Subject: [PATCH 16/28] refactor(shred-collector): import usage --- src/core/leader_schedule.zig | 1 - src/shred_collector/service.zig | 10 ++-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/core/leader_schedule.zig b/src/core/leader_schedule.zig index cc22c3452..0147bd2a6 100644 --- a/src/core/leader_schedule.zig +++ b/src/core/leader_schedule.zig @@ -6,7 +6,6 @@ const Allocator = std.mem.Allocator; const Bank = sig.accounts_db.Bank; const ChaChaRng = sig.rand.ChaChaRng; const Epoch = sig.core.Epoch; -const EpochStakes = sig.accounts_db.snapshots.EpochStakes; const Pubkey = sig.core.Pubkey; const Slot = sig.core.Slot; const WeightedRandomSampler = sig.rand.WeightedRandomSampler; diff --git a/src/shred_collector/service.zig b/src/shred_collector/service.zig index 38e7b8f8b..1ca9afa68 100644 --- a/src/shred_collector/service.zig +++ b/src/shred_collector/service.zig @@ -71,14 +71,8 @@ pub fn start( const turbine_socket = try bindUdpReusable(conf.turbine_recv_port); // receiver (threads) - const unverified_shred_channel = sig.sync.Channel(std.ArrayList(sig.net.Packet)).init( - deps.allocator, - 1000, - ); - const verified_shred_channel = sig.sync.Channel(std.ArrayList(sig.net.Packet)).init( - deps.allocator, - 1000, - ); + const unverified_shred_channel = Channel(ArrayList(Packet)).init(deps.allocator, 1000); + const verified_shred_channel = Channel(ArrayList(Packet)).init(deps.allocator, 1000); const shred_receiver = try arena.create(ShredReceiver); shred_receiver.* = ShredReceiver{ .allocator = deps.allocator, From 96ac55f8d599825c79d20cc97f0925ac5328da2d Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 12 Jun 2024 14:32:10 -0400 Subject: [PATCH 17/28] fix(shred-collector): get shred signed data bugs: - infinite loop in iterator due to not incrementing - adding in dataIndex instead of subtracting - invalid proof should be index != 0 --- src/accountsdb/snapshots.zig | 2 +- src/shred_collector/service.zig | 4 +- src/shred_collector/shred.zig | 68 +++++++++++++++++++++++++- src/shred_collector/shred_verifier.zig | 6 +-- 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/src/accountsdb/snapshots.zig b/src/accountsdb/snapshots.zig index 26b3bde9e..1466546cd 100644 --- a/src/accountsdb/snapshots.zig +++ b/src/accountsdb/snapshots.zig @@ -95,7 +95,7 @@ pub const VoteAccount = struct { pub const VoteState = struct { /// The variant of the rust enum - tag: u32, + tag: u32, // TODO: consider varint bincode serialization (in rust this is enum) /// the node that votes in this account node_pubkey: Pubkey, }; diff --git a/src/shred_collector/service.zig b/src/shred_collector/service.zig index 1ca9afa68..790e90a80 100644 --- a/src/shred_collector/service.zig +++ b/src/shred_collector/service.zig @@ -18,7 +18,7 @@ const Pubkey = sig.core.Pubkey; const RwMux = sig.sync.RwMux; const ServiceManager = sig.utils.service_manager.ServiceManager; const Slot = sig.core.Slot; -const SlotLeaderGetter = sig.core.leader_schedule.SlotLeaderProvider; +const SlotLeaderProvider = sig.core.leader_schedule.SlotLeaderProvider; const BasicShredTracker = shred_collector.shred_tracker.BasicShredTracker; const RepairPeerProvider = shred_collector.repair_service.RepairPeerProvider; @@ -48,7 +48,7 @@ pub const ShredCollectorDependencies = struct { gossip_table_rw: *RwMux(GossipTable), /// Shared state that is read from gossip my_shred_version: *const Atomic(u16), - leader_schedule: SlotLeaderGetter, + leader_schedule: SlotLeaderProvider, }; /// Start the Shred Collector. diff --git a/src/shred_collector/shred.zig b/src/shred_collector/shred.zig index f0ac3094d..1783f438d 100644 --- a/src/shred_collector/shred.zig +++ b/src/shred_collector/shred.zig @@ -276,6 +276,7 @@ fn getMerkleNode(shred: []const u8, start: usize, end: usize) !Hash { return hashv(&.{ MERKLE_HASH_PREFIX_LEAF, shred[start..end] }); } +/// [get_merkle_root](https://github.com/anza-xyz/agave/blob/ed500b5afc77bc78d9890d96455ea7a7f28edbf9/ledger/src/shred/merkle.rs#L702) fn calculateMerkleRoot(start_index: usize, start_node: Hash, proof: MerkleProofEntryList) !Hash { var index = start_index; var node = start_node; @@ -287,7 +288,7 @@ fn calculateMerkleRoot(start_index: usize, start_node: Hash, proof: MerkleProofE joinNodes(&other, &node.data); index = index >> 1; } - if (index == 0) return error.InvalidMerkleProf; + if (index != 0) return error.InvalidMerkleProof; return node; } @@ -340,7 +341,7 @@ fn codeIndex(shred: []const u8) ?usize { fn dataIndex(shred: []const u8) ?usize { const fec_set_index = getInt(u32, shred, 79) orelse return null; const layout_index = layout.getIndex(shred) orelse return null; - const index = checkedAdd(layout_index, fec_set_index) catch return null; + const index = checkedSub(layout_index, fec_set_index) catch return null; return @intCast(index); } @@ -358,6 +359,7 @@ pub fn Iterator(comptime Collection: type, comptime Item: type) type { if (self.index >= self.list.len) { return null; } + defer self.index += 1; return self.list.get(self.index); } }; @@ -625,3 +627,65 @@ fn testShredVariantRoundTrip(expected_byte: u8, start_variant: ShredVariant) !vo start_variant.resigned == end_variant.resigned, ); } + +test "getShredVariant" { + const variant = layout.getShredVariant(&test_data_shred).?; + try std.testing.expect(.Data == variant.shred_type); + try std.testing.expect(!variant.chained); + try std.testing.expect(!variant.resigned); + try std.testing.expect(6 == variant.proof_size); +} + +test "dataIndex" { + try std.testing.expect(31 == dataIndex(&test_data_shred).?); +} + +test "getIndex" { + try std.testing.expect(65 == layout.getIndex(&test_data_shred).?); +} + +test "getMerkleRoot" { + const variant = layout.getShredVariant(&test_data_shred).?; + const merkle_root = try getMerkleRoot(&test_data_shred, data_shred, variant); + const expected_signed_data = [_]u8{ + 224, 241, 85, 253, 247, 62, 137, 179, 152, 192, 186, 203, 121, 194, 178, 130, + 33, 181, 143, 156, 220, 150, 69, 197, 81, 97, 237, 11, 74, 156, 129, 134, + }; + try std.testing.expect(std.mem.eql(u8, &expected_signed_data, &merkle_root.data)); +} + +test "getSignature" { + const signature = layout.getSignature(&test_data_shred).?; + const expected_signature = [_]u8{ + 102, 205, 108, 67, 218, 3, 214, 186, 28, 110, 167, 22, 75, 135, 233, 156, 45, 215, 209, 1, + 253, 53, 142, 52, 6, 98, 158, 51, 157, 207, 190, 22, 96, 106, 68, 248, 244, 162, 13, 205, + 193, 194, 143, 192, 142, 141, 134, 85, 93, 252, 43, 200, 224, 101, 12, 28, 97, 202, 230, 215, + 34, 217, 20, 7, + }; + try std.testing.expect(std.mem.eql(u8, &expected_signature, &signature.data)); +} + +test "getSignedData" { + const signed_data = layout.getSignedData(&test_data_shred).?; + const expected_signed_data = [_]u8{ + 224, 241, 85, 253, 247, 62, 137, 179, 152, 192, 186, 203, 121, 194, 178, 130, + 33, 181, 143, 156, 220, 150, 69, 197, 81, 97, 237, 11, 74, 156, 129, 134, + }; + try std.testing.expect(std.mem.eql(u8, &expected_signed_data, &signed_data.data)); +} + +const test_data_shred = [_]u8{ + 102, 205, 108, 67, 218, 3, 214, 186, 28, 110, 167, 22, 75, 135, 233, 156, 45, 215, + 209, 1, 253, 53, 142, 52, 6, 98, 158, 51, 157, 207, 190, 22, 96, 106, 68, 248, + 244, 162, 13, 205, 193, 194, 143, 192, 142, 141, 134, 85, 93, 252, 43, 200, 224, 101, + 12, 28, 97, 202, 230, 215, 34, 217, 20, 7, 134, 105, 170, 47, 18, 0, 0, 0, + 0, 65, 0, 0, 0, 71, 176, 34, 0, 0, 0, 1, 0, 192, 88, +} ++ .{0} ** 996 ++ .{ + 247, 170, 109, 175, 191, 111, 108, 73, 56, 57, 34, 185, 81, 218, 60, 244, 53, 227, + 243, 72, 15, 175, 148, 58, 42, 0, 133, 246, 67, 118, 164, 221, 109, 136, 179, 199, + 15, 177, 139, 110, 105, 222, 165, 194, 78, 25, 172, 56, 165, 69, 28, 80, 215, 72, + 10, 21, 144, 236, 44, 107, 166, 65, 197, 164, 106, 113, 9, 68, 227, 37, 134, 158, + 192, 200, 22, 30, 244, 177, 106, 84, 161, 246, 35, 21, 26, 163, 104, 181, 13, 189, + 247, 250, 214, 101, 190, 52, 28, 152, 85, 9, 49, 168, 162, 199, 128, 242, 217, 219, + 71, 219, 72, 191, 107, 210, 46, 255, 206, 122, 234, 142, 229, 214, 240, 186, +}; diff --git a/src/shred_collector/shred_verifier.zig b/src/shred_collector/shred_verifier.zig index 8a19e3b0f..ccd225279 100644 --- a/src/shred_collector/shred_verifier.zig +++ b/src/shred_collector/shred_verifier.zig @@ -8,7 +8,7 @@ const ArrayList = std.ArrayList; const Atomic = std.atomic.Value; const Channel = sig.sync.Channel; -const SlotLeaderGetter = sig.core.leader_schedule.SlotLeaderProvider; +const SlotLeaderProvider = sig.core.leader_schedule.SlotLeaderProvider; const Packet = sig.net.Packet; /// Analogous to [run_shred_sigverify](https://github.com/anza-xyz/agave/blob/8c5a33a81a0504fd25d0465bed35d153ff84819f/turbine/src/sigverify_shreds.rs#L82) @@ -18,7 +18,7 @@ pub fn runShredVerifier( unverified_shred_receiver: *Channel(ArrayList(Packet)), /// me --> shred processor verified_shred_sender: *Channel(ArrayList(Packet)), - leader_schedule: sig.core.leader_schedule.SlotLeaderProvider, + leader_schedule: SlotLeaderProvider, ) !void { var verified_count: usize = 0; var buf = ArrayList(ArrayList(Packet)).init(unverified_shred_receiver.allocator); @@ -44,7 +44,7 @@ pub fn runShredVerifier( } /// verify_shred_cpu -fn verifyShred(packet: *const Packet, leader_schedule: SlotLeaderGetter) bool { +fn verifyShred(packet: *const Packet, leader_schedule: SlotLeaderProvider) bool { if (packet.flags.isSet(.discard)) return false; const shred = shred_layout.getShred(packet) orelse return false; const slot = shred_layout.getSlot(shred) orelse return false; From 9e534019ddabc72c37d2591b8dafac6e6967e2b5 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 12 Jun 2024 14:33:15 -0400 Subject: [PATCH 18/28] fix(accountsdb): zig fmt --- src/accountsdb/snapshots.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accountsdb/snapshots.zig b/src/accountsdb/snapshots.zig index 1466546cd..9cd7848e5 100644 --- a/src/accountsdb/snapshots.zig +++ b/src/accountsdb/snapshots.zig @@ -95,7 +95,7 @@ pub const VoteAccount = struct { pub const VoteState = struct { /// The variant of the rust enum - tag: u32, // TODO: consider varint bincode serialization (in rust this is enum) + tag: u32, // TODO: consider varint bincode serialization (in rust this is enum) /// the node that votes in this account node_pubkey: Pubkey, }; From a8eefe427e07d583cfc087300cb8ff59a0ba618c Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 12 Jun 2024 15:14:56 -0400 Subject: [PATCH 19/28] fix(cmd): re-enable most of snapshot validations --- src/cmd/cmd.zig | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/cmd/cmd.zig b/src/cmd/cmd.zig index e97dbe3c5..8a8d55ef1 100644 --- a/src/cmd/cmd.zig +++ b/src/cmd/cmd.zig @@ -742,7 +742,7 @@ fn loadSnapshot( allocator: Allocator, logger: Logger, gossip_service: *GossipService, - validate: bool, + validate_genesis: bool, ) !*LoadedSnapshot { const output = try allocator.create(LoadedSnapshot); var snapshots = try getOrDownloadSnapshots( @@ -795,24 +795,25 @@ fn loadSnapshot( logger.infof("validating bank...", .{}); output.bank = Bank.init(&output.accounts_db, bank_fields); - // TODO: remove this condition once these validations work reliably on all clusters - if (validate) { - try Bank.validateBankFields(output.bank.bank_fields, &output.genesis_config); + Bank.validateBankFields(output.bank.bank_fields, &output.genesis_config) catch |e| switch (e) { + // TODO: remove when genesis validation works on all clusters + error.BankAndGenesisMismatch => if (validate_genesis) return e, + else => return e, + }; - // validate the status cache - logger.infof("validating status cache...", .{}); - var status_cache = readStatusCache(allocator, snapshot_dir_str) catch |err| { - if (err == error.StatusCacheNotFound) { - logger.errf("status-cache.bin not found - expecting {s}/snapshots/status-cache to exist", .{snapshot_dir_str}); - } - return err; - }; - defer status_cache.deinit(); + // validate the status cache + logger.infof("validating status cache...", .{}); + var status_cache = readStatusCache(allocator, snapshot_dir_str) catch |err| { + if (err == error.StatusCacheNotFound) { + logger.errf("status-cache.bin not found - expecting {s}/snapshots/status-cache to exist", .{snapshot_dir_str}); + } + return err; + }; + defer status_cache.deinit(); - var slot_history = try output.accounts_db.getSlotHistory(); - defer slot_history.deinit(output.accounts_db.allocator); - try status_cache.validate(allocator, bank_fields.slot, &slot_history); - } + var slot_history = try output.accounts_db.getSlotHistory(); + defer slot_history.deinit(output.accounts_db.allocator); + try status_cache.validate(allocator, bank_fields.slot, &slot_history); logger.infof("accounts-db setup done...", .{}); From 0363821853b2957b67eced46c9a44cefa4d88661 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 12 Jun 2024 15:57:30 -0400 Subject: [PATCH 20/28] feat(shred-collector): start from snapshot slot --- src/cmd/cmd.zig | 4 +++- src/shred_collector/service.zig | 1 + src/shred_collector/shred_receiver.zig | 7 +++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/cmd/cmd.zig b/src/cmd/cmd.zig index cd1e36865..624bd0022 100644 --- a/src/cmd/cmd.zig +++ b/src/cmd/cmd.zig @@ -445,9 +445,11 @@ fn validator() !void { const leader_provider = leader_schedule.provider(); // shred collector + var shred_col_conf = config.current.shred_collector; + shred_col_conf.start_slot = shred_col_conf.start_slot orelse snapshot.bank.bank_fields.slot; var rng = std.rand.DefaultPrng.init(@bitCast(std.time.timestamp())); var shred_collector = try sig.shred_collector.start( - config.current.shred_collector, + shred_col_conf, ShredCollectorDependencies{ .allocator = allocator, .logger = app_base.logger, diff --git a/src/shred_collector/service.zig b/src/shred_collector/service.zig index 790e90a80..f50656f63 100644 --- a/src/shred_collector/service.zig +++ b/src/shred_collector/service.zig @@ -84,6 +84,7 @@ pub fn start( .unverified_shred_sender = unverified_shred_channel, .shred_version = deps.my_shred_version, .metrics = try ShredReceiverMetrics.init(), + .root_slot = if (conf.start_slot) |s| s - 1 else 0, }; try service_manager.spawn("Shred Receiver", ShredReceiver.run, .{shred_receiver}); diff --git a/src/shred_collector/shred_receiver.zig b/src/shred_collector/shred_receiver.zig index 8db46fcce..05a87a5c7 100644 --- a/src/shred_collector/shred_receiver.zig +++ b/src/shred_collector/shred_receiver.zig @@ -35,6 +35,7 @@ pub const ShredReceiver = struct { unverified_shred_sender: *Channel(ArrayList(Packet)), shred_version: *const Atomic(u16), metrics: ShredReceiverMetrics, + root_slot: Slot, const Self = @This(); @@ -118,10 +119,8 @@ pub const ShredReceiver = struct { try self.handlePing(packet, responses); packet.flags.set(.discard); } else { - // TODO set correct values once using snapshot + blockstore - const root = 0; - const max_slot = std.math.maxInt(Slot); - if (shouldDiscardShred(packet, root, shred_version, max_slot)) { + const max_slot = std.math.maxInt(Slot); // TODO agave uses BankForks for this + if (shouldDiscardShred(packet, self.root_slot, shred_version, max_slot)) { packet.flags.set(.discard); } } From 0eb0afa3621b5a379678670a6681116e957140e7 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Thu, 13 Jun 2024 14:46:51 -0400 Subject: [PATCH 21/28] feat(cmd): log error on bank validation failure --- src/cmd/cmd.zig | 7 +++++-- src/shred_collector/shred_receiver.zig | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/cmd/cmd.zig b/src/cmd/cmd.zig index 624bd0022..7daa45927 100644 --- a/src/cmd/cmd.zig +++ b/src/cmd/cmd.zig @@ -796,10 +796,13 @@ fn loadSnapshot( logger.infof("validating bank...", .{}); output.bank = Bank.init(&output.accounts_db, bank_fields); - Bank.validateBankFields(output.bank.bank_fields, &output.genesis_config) catch |e| switch (e) { // TODO: remove when genesis validation works on all clusters - error.BankAndGenesisMismatch => if (validate_genesis) return e, + error.BankAndGenesisMismatch => if (validate_genesis) { + return e; + } else { + logger.err("Bank failed genesis validation."); + }, else => return e, }; diff --git a/src/shred_collector/shred_receiver.zig b/src/shred_collector/shred_receiver.zig index 05a87a5c7..5828bf2a6 100644 --- a/src/shred_collector/shred_receiver.zig +++ b/src/shred_collector/shred_receiver.zig @@ -35,7 +35,7 @@ pub const ShredReceiver = struct { unverified_shred_sender: *Channel(ArrayList(Packet)), shred_version: *const Atomic(u16), metrics: ShredReceiverMetrics, - root_slot: Slot, + root_slot: Slot, // TODO: eventually, this should be handled by BankForks const Self = @This(); From e2c69065045152e509749f7e191b1de533a287f5 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 26 Jun 2024 10:41:50 -0400 Subject: [PATCH 22/28] fix(accountsdb): fix stakedNodes memoization --- src/accountsdb/snapshots.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/accountsdb/snapshots.zig b/src/accountsdb/snapshots.zig index ae1965427..07877a88d 100644 --- a/src/accountsdb/snapshots.zig +++ b/src/accountsdb/snapshots.zig @@ -61,7 +61,12 @@ pub const VoteAccounts = struct { pub const @"!bincode-config:staked_nodes" = bincode.FieldConfig(?HashMap(Pubkey, u64)){ .skip = true }; - pub fn stakedNodes(self: *@This(), allocator: std.mem.Allocator) !*const HashMap(Pubkey, u64) { + const Self = @This(); + + pub fn stakedNodes(self: *Self, allocator: std.mem.Allocator) !*const HashMap(Pubkey, u64) { + if (self.staked_nodes) |*staked_nodes| { + return staked_nodes; + } const vote_accounts = self.vote_accounts; var staked_nodes = HashMap(Pubkey, u64).init(allocator); var iter = vote_accounts.iterator(); From 0c5121e94bb9ed2683a8c5e153ab84a2f84ae767 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Thu, 27 Jun 2024 20:17:49 -0400 Subject: [PATCH 23/28] fix(cmd): errdefer deinit accountsdb items --- src/cmd/cmd.zig | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/cmd/cmd.zig b/src/cmd/cmd.zig index 625269c2d..1ca2d6847 100644 --- a/src/cmd/cmd.zig +++ b/src/cmd/cmd.zig @@ -714,7 +714,6 @@ fn spawnLogger() !Logger { const LoadedSnapshot = struct { allocator: Allocator, accounts_db: AccountsDB, - snapshots: SnapshotFieldsAndPaths, snapshot_fields: sig.accounts_db.SnapshotFields, /// contains pointers to `accounts_db` and `snapshot_fields` bank: Bank, @@ -723,8 +722,6 @@ const LoadedSnapshot = struct { pub fn deinit(self: *@This()) void { self.genesis_config.deinit(self.allocator); self.snapshot_fields.deinit(self.allocator); - self.accounts_db.deinit(); - self.snapshots.deinit(self.allocator); self.accounts_db.deinit(false); // keep index files on disk self.allocator.destroy(self); } @@ -737,11 +734,13 @@ fn loadSnapshot( validate_genesis: bool, ) !*LoadedSnapshot { const output = try allocator.create(LoadedSnapshot); + errdefer allocator.destroy(output); var snapshots = try getOrDownloadSnapshots( allocator, logger, gossip_service, ); + defer snapshots.deinit(allocator); logger.infof("full snapshot: {s}", .{snapshots.full_path}); if (snapshots.incremental_path) |inc_path| { @@ -761,6 +760,7 @@ fn loadSnapshot( logger, config.current.accounts_db, ); + errdefer output.accounts_db.deinit(false); output.snapshot_fields = try output.accounts_db.loadWithDefaults( &snapshots, @@ -768,6 +768,7 @@ fn loadSnapshot( n_threads_snapshot_load, true, // validate too ); + errdefer output.snapshot_fields.deinit(allocator); const bank_fields = &output.snapshot_fields.bank_fields; @@ -779,6 +780,7 @@ fn loadSnapshot( } return err; }; + errdefer output.genesis_config.deinit(allocator); logger.infof("validating bank...", .{}); output.bank = Bank.init(&output.accounts_db, bank_fields); From 7514974fdfbaf9917d8fa4fbea575b0eaff48105 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Thu, 27 Jun 2024 20:18:39 -0400 Subject: [PATCH 24/28] refactor(cmd): rename loadSnapshot to loadFromSnapshot --- src/cmd/cmd.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cmd/cmd.zig b/src/cmd/cmd.zig index 1ca2d6847..e69091796 100644 --- a/src/cmd/cmd.zig +++ b/src/cmd/cmd.zig @@ -426,7 +426,7 @@ fn validator() !void { defer gossip_service.deinit(); defer gossip_manager.deinit(); - const snapshot = try loadSnapshot(allocator, app_base.logger, gossip_service, false); + const snapshot = try loadFromSnapshot(allocator, app_base.logger, gossip_service, false); // leader schedule var leader_schedule = try getLeaderScheduleFromCli(allocator) orelse @@ -465,7 +465,7 @@ fn printLeaderSchedule() !void { var gossip_service, var gossip_manager = try startGossip(allocator, &app_base, &.{}); defer gossip_service.deinit(); defer gossip_manager.deinit(); - const snapshot = try loadSnapshot(allocator, app_base.logger, gossip_service, false); + const snapshot = try loadFromSnapshot(allocator, app_base.logger, gossip_service, false); break :b try leaderScheduleFromBank(allocator, &snapshot.bank); }; @@ -727,7 +727,7 @@ const LoadedSnapshot = struct { } }; -fn loadSnapshot( +fn loadFromSnapshot( allocator: Allocator, logger: Logger, gossip_service: *GossipService, From bd139210a161466257ea96e8c29b289b2e28e848 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Thu, 27 Jun 2024 20:21:21 -0400 Subject: [PATCH 25/28] feat(cmd): log when downloading a snapshot for the leader schedule --- src/cmd/cmd.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cmd/cmd.zig b/src/cmd/cmd.zig index e69091796..4f31478e4 100644 --- a/src/cmd/cmd.zig +++ b/src/cmd/cmd.zig @@ -462,6 +462,7 @@ fn printLeaderSchedule() !void { var app_base = try AppBase.init(allocator); const leader_schedule = try getLeaderScheduleFromCli(allocator) orelse b: { + app_base.logger.info("Downloading a snapshot to calculate the leader schedule."); var gossip_service, var gossip_manager = try startGossip(allocator, &app_base, &.{}); defer gossip_service.deinit(); defer gossip_manager.deinit(); From 4abb2037aa384b05afe2fad65d54871838d87ca3 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Thu, 27 Jun 2024 20:23:26 -0400 Subject: [PATCH 26/28] refactor(config): move leader_schedule_path field out of general section --- src/cmd/config.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmd/config.zig b/src/cmd/config.zig index 48ea6528b..b3ddc415b 100644 --- a/src/cmd/config.zig +++ b/src/cmd/config.zig @@ -6,10 +6,10 @@ pub const Config = struct { gossip: GossipConfig = .{}, shred_collector: ShredCollectorConfig = shred_collector_defaults, accounts_db: AccountsDBConfig = .{}, + leader_schedule_path: ?[]const u8 = null, // general config log_level: []const u8 = "debug", metrics_port: u16 = 12345, - leader_schedule_path: ?[]const u8 = null, }; pub const current: *Config = &default_validator_config; From 2b3c85134d4752397eeb323eca9a5063969cfc7e Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Thu, 27 Jun 2024 20:33:05 -0400 Subject: [PATCH 27/28] test(accountsdb): assert MINIMUM_SLOTS_PER_EPOCH works properly in getSlotsInEpoch calculation --- src/accountsdb/genesis_config.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/accountsdb/genesis_config.zig b/src/accountsdb/genesis_config.zig index 3725706e2..0b342e09a 100644 --- a/src/accountsdb/genesis_config.zig +++ b/src/accountsdb/genesis_config.zig @@ -137,6 +137,7 @@ pub const EpochSchedule = extern struct { /// get the length of the given epoch (in slots) pub fn getSlotsInEpoch(self: *const EpochSchedule, epoch: Epoch) Slot { + comptime std.debug.assert(std.math.isPowerOfTwo(MINIMUM_SLOTS_PER_EPOCH)); return if (epoch < self.first_normal_epoch) @as(Slot, 1) <<| epoch +| @ctz(MINIMUM_SLOTS_PER_EPOCH) else From 0f3c5c9d0c22bd69c3323c8fe341b28e12d079cc Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Thu, 27 Jun 2024 21:02:15 -0400 Subject: [PATCH 28/28] refactor(shred-collector): check exit bool in while condition --- src/shred_collector/shred_processor.zig | 4 +--- src/shred_collector/shred_verifier.zig | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/shred_collector/shred_processor.zig b/src/shred_collector/shred_processor.zig index d8485df66..5d83674dd 100644 --- a/src/shred_collector/shred_processor.zig +++ b/src/shred_collector/shred_processor.zig @@ -25,10 +25,9 @@ pub fn runShredProcessor( ) !void { var buf = ArrayList(ArrayList(Packet)).init(allocator); var error_context = ErrorContext{}; - while (true) { + while (!exit.load(.unordered)) { try verified_shred_receiver.tryDrainRecycle(&buf); if (buf.items.len == 0) { - if (exit.load(.monotonic)) return; std.time.sleep(10 * std.time.ns_per_ms); continue; } @@ -43,7 +42,6 @@ pub fn runShredProcessor( }; }; } - if (exit.load(.monotonic)) return; } } diff --git a/src/shred_collector/shred_verifier.zig b/src/shred_collector/shred_verifier.zig index ae68c161e..2404a6520 100644 --- a/src/shred_collector/shred_verifier.zig +++ b/src/shred_collector/shred_verifier.zig @@ -22,10 +22,9 @@ pub fn runShredVerifier( ) !void { var verified_count: usize = 0; var buf = ArrayList(ArrayList(Packet)).init(unverified_shred_receiver.allocator); - while (true) { + while (!exit.load(.unordered)) { try unverified_shred_receiver.tryDrainRecycle(&buf); if (buf.items.len == 0) { - if (exit.load(.monotonic)) return; std.time.sleep(10 * std.time.ns_per_ms); continue; }