tigerbeetle-node 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +47 -47
  2. package/dist/benchmark.js +15 -15
  3. package/dist/benchmark.js.map +1 -1
  4. package/dist/index.d.ts +66 -61
  5. package/dist/index.js +66 -61
  6. package/dist/index.js.map +1 -1
  7. package/dist/test.js +1 -1
  8. package/dist/test.js.map +1 -1
  9. package/package.json +14 -16
  10. package/scripts/download_node_headers.sh +3 -1
  11. package/src/index.ts +5 -0
  12. package/src/node.zig +18 -19
  13. package/src/tigerbeetle/scripts/benchmark.bat +47 -46
  14. package/src/tigerbeetle/scripts/benchmark.sh +25 -10
  15. package/src/tigerbeetle/scripts/install.sh +2 -1
  16. package/src/tigerbeetle/scripts/install_zig.bat +109 -109
  17. package/src/tigerbeetle/scripts/install_zig.sh +18 -18
  18. package/src/tigerbeetle/scripts/upgrade_ubuntu_kernel.sh +12 -3
  19. package/src/tigerbeetle/scripts/vopr.bat +47 -47
  20. package/src/tigerbeetle/scripts/vopr.sh +5 -5
  21. package/src/tigerbeetle/src/benchmark.zig +17 -9
  22. package/src/tigerbeetle/src/benchmark_array_search.zig +317 -0
  23. package/src/tigerbeetle/src/benchmarks/perf.zig +299 -0
  24. package/src/tigerbeetle/src/c/tb_client/context.zig +103 -0
  25. package/src/tigerbeetle/src/c/tb_client/packet.zig +80 -0
  26. package/src/tigerbeetle/src/c/tb_client/signal.zig +288 -0
  27. package/src/tigerbeetle/src/c/tb_client/thread.zig +329 -0
  28. package/src/tigerbeetle/src/c/tb_client.h +201 -0
  29. package/src/tigerbeetle/src/c/tb_client.zig +101 -0
  30. package/src/tigerbeetle/src/c/test.zig +1 -0
  31. package/src/tigerbeetle/src/cli.zig +142 -83
  32. package/src/tigerbeetle/src/config.zig +136 -23
  33. package/src/tigerbeetle/src/demo.zig +12 -8
  34. package/src/tigerbeetle/src/demo_03_create_transfers.zig +3 -3
  35. package/src/tigerbeetle/src/demo_04_create_pending_transfers.zig +10 -10
  36. package/src/tigerbeetle/src/demo_05_post_pending_transfers.zig +7 -7
  37. package/src/tigerbeetle/src/demo_06_void_pending_transfers.zig +3 -3
  38. package/src/tigerbeetle/src/demo_07_lookup_transfers.zig +1 -1
  39. package/src/tigerbeetle/src/ewah.zig +318 -0
  40. package/src/tigerbeetle/src/ewah_benchmark.zig +121 -0
  41. package/src/tigerbeetle/src/eytzinger_benchmark.zig +317 -0
  42. package/src/tigerbeetle/src/fifo.zig +17 -1
  43. package/src/tigerbeetle/src/io/darwin.zig +12 -10
  44. package/src/tigerbeetle/src/io/linux.zig +25 -9
  45. package/src/tigerbeetle/src/io/windows.zig +13 -9
  46. package/src/tigerbeetle/src/iops.zig +101 -0
  47. package/src/tigerbeetle/src/lsm/binary_search.zig +214 -0
  48. package/src/tigerbeetle/src/lsm/bloom_filter.zig +82 -0
  49. package/src/tigerbeetle/src/lsm/compaction.zig +603 -0
  50. package/src/tigerbeetle/src/lsm/composite_key.zig +75 -0
  51. package/src/tigerbeetle/src/lsm/direction.zig +11 -0
  52. package/src/tigerbeetle/src/lsm/eytzinger.zig +587 -0
  53. package/src/tigerbeetle/src/lsm/forest.zig +630 -0
  54. package/src/tigerbeetle/src/lsm/grid.zig +473 -0
  55. package/src/tigerbeetle/src/lsm/groove.zig +939 -0
  56. package/src/tigerbeetle/src/lsm/k_way_merge.zig +452 -0
  57. package/src/tigerbeetle/src/lsm/level_iterator.zig +296 -0
  58. package/src/tigerbeetle/src/lsm/manifest.zig +680 -0
  59. package/src/tigerbeetle/src/lsm/manifest_level.zig +1169 -0
  60. package/src/tigerbeetle/src/lsm/manifest_log.zig +904 -0
  61. package/src/tigerbeetle/src/lsm/node_pool.zig +231 -0
  62. package/src/tigerbeetle/src/lsm/posted_groove.zig +399 -0
  63. package/src/tigerbeetle/src/lsm/segmented_array.zig +998 -0
  64. package/src/tigerbeetle/src/lsm/set_associative_cache.zig +844 -0
  65. package/src/tigerbeetle/src/lsm/table.zig +932 -0
  66. package/src/tigerbeetle/src/lsm/table_immutable.zig +196 -0
  67. package/src/tigerbeetle/src/lsm/table_iterator.zig +295 -0
  68. package/src/tigerbeetle/src/lsm/table_mutable.zig +123 -0
  69. package/src/tigerbeetle/src/lsm/test.zig +429 -0
  70. package/src/tigerbeetle/src/lsm/tree.zig +1085 -0
  71. package/src/tigerbeetle/src/main.zig +121 -95
  72. package/src/tigerbeetle/src/message_bus.zig +49 -48
  73. package/src/tigerbeetle/src/message_pool.zig +19 -3
  74. package/src/tigerbeetle/src/ring_buffer.zig +172 -31
  75. package/src/tigerbeetle/src/simulator.zig +171 -43
  76. package/src/tigerbeetle/src/state_machine.zig +1026 -599
  77. package/src/tigerbeetle/src/storage.zig +46 -16
  78. package/src/tigerbeetle/src/test/cluster.zig +257 -78
  79. package/src/tigerbeetle/src/test/message_bus.zig +15 -24
  80. package/src/tigerbeetle/src/test/network.zig +26 -17
  81. package/src/tigerbeetle/src/test/packet_simulator.zig +14 -1
  82. package/src/tigerbeetle/src/test/state_checker.zig +10 -6
  83. package/src/tigerbeetle/src/test/state_machine.zig +159 -68
  84. package/src/tigerbeetle/src/test/storage.zig +137 -49
  85. package/src/tigerbeetle/src/tigerbeetle.zig +5 -0
  86. package/src/tigerbeetle/src/unit_tests.zig +8 -0
  87. package/src/tigerbeetle/src/util.zig +51 -0
  88. package/src/tigerbeetle/src/vsr/client.zig +21 -7
  89. package/src/tigerbeetle/src/vsr/journal.zig +1429 -514
  90. package/src/tigerbeetle/src/vsr/replica.zig +1855 -550
  91. package/src/tigerbeetle/src/vsr/superblock.zig +1743 -0
  92. package/src/tigerbeetle/src/vsr/superblock_client_table.zig +258 -0
  93. package/src/tigerbeetle/src/vsr/superblock_free_set.zig +644 -0
  94. package/src/tigerbeetle/src/vsr/superblock_manifest.zig +546 -0
  95. package/src/tigerbeetle/src/vsr.zig +134 -52
  96. package/.yarn/releases/yarn-berry.cjs +0 -55
  97. package/.yarnrc.yml +0 -1
  98. package/scripts/postinstall.sh +0 -6
  99. package/yarn.lock +0 -42
@@ -17,634 +17,890 @@ const CreateTransfersResult = tb.CreateTransfersResult;
17
17
 
18
18
  const CreateAccountResult = tb.CreateAccountResult;
19
19
  const CreateTransferResult = tb.CreateTransferResult;
20
- const LookupAccountResult = tb.LookupAccountResult;
21
-
22
- const HashMapAccounts = std.AutoHashMap(u128, Account);
23
- const HashMapTransfers = std.AutoHashMap(u128, Transfer);
24
- const HashMapPosted = std.AutoHashMap(u128, bool);
25
-
26
- pub const StateMachine = struct {
27
- pub const Operation = enum(u8) {
28
- /// Operations reserved by VR protocol (for all state machines):
29
- reserved,
30
- init,
31
- register,
32
-
33
- /// Operations exported by TigerBeetle:
34
- create_accounts,
35
- create_transfers,
36
- lookup_accounts,
37
- lookup_transfers,
38
- };
39
20
 
40
- allocator: mem.Allocator,
41
- prepare_timestamp: u64,
42
- commit_timestamp: u64,
43
- accounts: HashMapAccounts,
44
- transfers: HashMapTransfers,
45
- posted: HashMapPosted,
46
-
47
- pub fn init(
48
- allocator: mem.Allocator,
49
- accounts_max: usize,
50
- transfers_max: usize,
51
- transfers_pending_max: usize,
52
- ) !StateMachine {
53
- var accounts = HashMapAccounts.init(allocator);
54
- errdefer accounts.deinit();
55
- try accounts.ensureTotalCapacity(@intCast(u32, accounts_max));
56
-
57
- var transfers = HashMapTransfers.init(allocator);
58
- errdefer transfers.deinit();
59
- try transfers.ensureTotalCapacity(@intCast(u32, transfers_max));
60
-
61
- var posted = HashMapPosted.init(allocator);
62
- errdefer posted.deinit();
63
- try posted.ensureTotalCapacity(@intCast(u32, transfers_pending_max));
64
-
65
- // TODO After recovery, set prepare_timestamp max(wall clock, op timestamp).
66
- // TODO After recovery, set commit_timestamp max(wall clock, commit timestamp).
67
-
68
- return StateMachine{
69
- .allocator = allocator,
70
- .prepare_timestamp = 0,
71
- .commit_timestamp = 0,
72
- .accounts = accounts,
73
- .transfers = transfers,
74
- .posted = posted,
75
- };
76
- }
21
+ pub fn StateMachineType(comptime Storage: type) type {
22
+ return struct {
23
+ const StateMachine = @This();
24
+
25
+ const Grid = @import("lsm/grid.zig").GridType(Storage);
26
+ const GrooveType = @import("lsm/groove.zig").GrooveType;
27
+ const ForestType = @import("lsm/forest.zig").ForestType;
28
+
29
+ const AccountsGroove = GrooveType(
30
+ Storage,
31
+ Account,
32
+ .{
33
+ .ignored = &[_][]const u8{ "reserved", "flags" },
34
+ .derived = .{},
35
+ },
36
+ );
37
+ const TransfersGroove = GrooveType(
38
+ Storage,
39
+ Transfer,
40
+ .{
41
+ .ignored = &[_][]const u8{ "reserved", "flags" },
42
+ .derived = .{},
43
+ },
44
+ );
45
+ const PostedGroove = @import("lsm/posted_groove.zig").PostedGrooveType(Storage);
77
46
 
78
- pub fn deinit(self: *StateMachine) void {
79
- self.accounts.deinit();
80
- self.transfers.deinit();
81
- self.posted.deinit();
82
- }
47
+ pub const Forest = ForestType(Storage, .{
48
+ .accounts = AccountsGroove,
49
+ .transfers = TransfersGroove,
50
+ .posted = PostedGroove,
51
+ });
83
52
 
84
- pub fn Event(comptime operation: Operation) type {
85
- return switch (operation) {
86
- .create_accounts => Account,
87
- .create_transfers => Transfer,
88
- .lookup_accounts => u128,
89
- .lookup_transfers => u128,
90
- else => unreachable,
53
+ pub const Operation = enum(u8) {
54
+ /// Operations reserved by VR protocol (for all state machines):
55
+ reserved,
56
+ root,
57
+ register,
58
+
59
+ /// Operations exported by TigerBeetle:
60
+ create_accounts,
61
+ create_transfers,
62
+ lookup_accounts,
63
+ lookup_transfers,
91
64
  };
92
- }
93
65
 
94
- pub fn Result(comptime operation: Operation) type {
95
- return switch (operation) {
96
- .create_accounts => CreateAccountsResult,
97
- .create_transfers => CreateTransfersResult,
98
- .lookup_accounts => Account,
99
- .lookup_transfers => Transfer,
100
- else => unreachable,
66
+ pub const Options = struct {
67
+ lsm_forest_node_count: u32,
68
+ cache_size_accounts: u32,
69
+ cache_size_transfers: u32,
70
+ cache_size_posted: u32,
101
71
  };
102
- }
103
72
 
104
- pub fn prepare(self: *StateMachine, realtime: i64, operation: Operation, input: []u8) void {
105
- switch (operation) {
106
- .init => unreachable,
107
- .register => {},
108
- .create_accounts => self.prepare_timestamps(realtime, .create_accounts, input),
109
- .create_transfers => self.prepare_timestamps(realtime, .create_transfers, input),
110
- .lookup_accounts => {},
111
- .lookup_transfers => {},
112
- else => unreachable,
73
+ prepare_timestamp: u64,
74
+ commit_timestamp: u64,
75
+ forest: Forest,
76
+
77
+ prefetch_input: ?[]align(16) const u8 = null,
78
+ prefetch_callback: ?fn (*StateMachine) void = null,
79
+ // TODO(ifreund): use a union for these to save memory, likely an extern union
80
+ // so that we can safetly @ptrCast() until @fieldParentPtr() is implemented
81
+ // for unions. See: https://github.com/ziglang/zig/issues/6611
82
+ prefetch_accounts_context: AccountsGroove.PrefetchContext = undefined,
83
+ prefetch_transfers_context: TransfersGroove.PrefetchContext = undefined,
84
+ prefetch_posted_context: PostedGroove.PrefetchContext = undefined,
85
+
86
+ open_callback: ?fn (*StateMachine) void = null,
87
+ compact_callback: ?fn (*StateMachine) void = null,
88
+ checkpoint_callback: ?fn (*StateMachine) void = null,
89
+
90
+ pub fn init(allocator: mem.Allocator, grid: *Grid, options: Options) !StateMachine {
91
+ var forest = try Forest.init(
92
+ allocator,
93
+ grid,
94
+ options.lsm_forest_node_count,
95
+ .{
96
+ .accounts = .{
97
+ .cache_size = options.cache_size_accounts,
98
+ .commit_count_max = 8191,
99
+ },
100
+ .transfers = .{
101
+ .cache_size = options.cache_size_transfers,
102
+ .commit_count_max = 8191 * 2,
103
+ },
104
+ .posted = .{
105
+ .cache_size = options.cache_size_posted,
106
+ .commit_count_max = 8191 * 2,
107
+ },
108
+ },
109
+ );
110
+ errdefer forest.deinit(allocator);
111
+
112
+ return StateMachine{
113
+ .prepare_timestamp = 0,
114
+ .commit_timestamp = 0,
115
+ .forest = forest,
116
+ };
113
117
  }
114
- }
115
118
 
116
- fn prepare_timestamps(
117
- self: *StateMachine,
118
- realtime: i64,
119
- comptime operation: Operation,
120
- input: []u8,
121
- ) void {
122
- // Guard against the wall clock going backwards by taking the max with timestamps issued:
123
- self.prepare_timestamp = math.max(
124
- // The cluster `commit_timestamp` may be ahead of our `prepare_timestamp` because this
125
- // may be our first prepare as a recently elected leader:
126
- math.max(self.prepare_timestamp, self.commit_timestamp) + 1,
127
- @intCast(u64, realtime),
128
- );
129
- assert(self.prepare_timestamp > self.commit_timestamp);
130
- var sum_reserved_timestamps: usize = 0;
131
- var events = mem.bytesAsSlice(Event(operation), input);
132
- for (events) |*event| {
133
- sum_reserved_timestamps += event.timestamp;
134
- self.prepare_timestamp += 1;
135
- event.timestamp = self.prepare_timestamp;
119
+ pub fn deinit(self: *StateMachine, allocator: mem.Allocator) void {
120
+ self.forest.deinit(allocator);
136
121
  }
137
- // The client is responsible for ensuring that timestamps are reserved:
138
- // Use a single branch condition to detect non-zero reserved timestamps.
139
- // Summing then branching once is faster than branching every iteration of the loop.
140
- assert(sum_reserved_timestamps == 0);
141
- }
142
122
 
143
- pub fn commit(
144
- self: *StateMachine,
145
- client: u128,
146
- operation: Operation,
147
- input: []const u8,
148
- output: []u8,
149
- ) usize {
150
- _ = client;
151
-
152
- return switch (operation) {
153
- .init => unreachable,
154
- .register => 0,
155
- .create_accounts => self.execute(.create_accounts, input, output),
156
- .create_transfers => self.execute(.create_transfers, input, output),
157
- .lookup_accounts => self.execute_lookup_accounts(input, output),
158
- .lookup_transfers => self.execute_lookup_transfers(input, output),
159
- else => unreachable,
160
- };
161
- }
123
+ pub fn Event(comptime operation: Operation) type {
124
+ return switch (operation) {
125
+ .create_accounts => Account,
126
+ .create_transfers => Transfer,
127
+ .lookup_accounts => u128,
128
+ .lookup_transfers => u128,
129
+ else => unreachable,
130
+ };
131
+ }
162
132
 
163
- fn execute(
164
- self: *StateMachine,
165
- comptime operation: Operation,
166
- input: []const u8,
167
- output: []u8,
168
- ) usize {
169
- comptime assert(operation != .lookup_accounts and operation != .lookup_transfers);
170
-
171
- const events = mem.bytesAsSlice(Event(operation), input);
172
- var results = mem.bytesAsSlice(Result(operation), output);
173
- var count: usize = 0;
174
-
175
- var chain: ?usize = null;
176
- var chain_broken = false;
177
-
178
- for (events) |*event, index| {
179
- if (event.flags.linked and chain == null) {
180
- chain = index;
181
- assert(chain_broken == false);
182
- }
183
- const result = if (chain_broken) .linked_event_failed else switch (operation) {
184
- .create_accounts => self.create_account(event),
185
- .create_transfers => self.create_transfer(event),
133
+ pub fn Result(comptime operation: Operation) type {
134
+ return switch (operation) {
135
+ .create_accounts => CreateAccountsResult,
136
+ .create_transfers => CreateTransfersResult,
137
+ .lookup_accounts => Account,
138
+ .lookup_transfers => Transfer,
186
139
  else => unreachable,
187
140
  };
188
- log.debug("{s} {}/{}: {}: {}", .{
189
- @tagName(operation),
190
- index + 1,
191
- events.len,
192
- result,
193
- event,
194
- });
195
- if (result != .ok) {
196
- if (chain) |chain_start_index| {
197
- if (!chain_broken) {
198
- chain_broken = true;
199
- // Rollback events in LIFO order, excluding this event that broke the chain:
200
- self.rollback(operation, input, chain_start_index, index);
201
- // Add errors for rolled back events in FIFO order:
202
- var chain_index = chain_start_index;
203
- while (chain_index < index) : (chain_index += 1) {
204
- results[count] = .{
205
- .index = @intCast(u32, chain_index),
206
- .result = .linked_event_failed,
207
- };
208
- count += 1;
209
- }
210
- } else {
211
- assert(result == .linked_event_failed);
212
- }
213
- }
214
- results[count] = .{ .index = @intCast(u32, index), .result = result };
215
- count += 1;
216
- }
217
- if (!event.flags.linked and chain != null) {
218
- chain = null;
219
- chain_broken = false;
220
- }
221
141
  }
222
- // TODO client.zig: Validate that batch chains are always well-formed and closed.
223
- // This is programming error and we should raise an exception for this in the client ASAP.
224
- assert(chain == null);
225
- assert(chain_broken == false);
226
142
 
227
- return @sizeOf(Result(operation)) * count;
228
- }
143
+ pub fn open(self: *StateMachine, callback: fn (*StateMachine) void) void {
144
+ assert(self.open_callback == null);
145
+ self.open_callback = callback;
146
+
147
+ self.forest.open(forest_open_callback);
148
+ }
149
+
150
+ fn forest_open_callback(forest: *Forest) void {
151
+ const self = @fieldParentPtr(StateMachine, "forest", forest);
152
+ assert(self.open_callback != null);
229
153
 
230
- fn rollback(
231
- self: *StateMachine,
232
- comptime operation: Operation,
233
- input: []const u8,
234
- chain_start_index: usize,
235
- chain_error_index: usize,
236
- ) void {
237
- const events = mem.bytesAsSlice(Event(operation), input);
238
-
239
- // We commit events in FIFO order.
240
- // We must therefore rollback events in LIFO order with a reverse loop.
241
- // We do not rollback `self.commit_timestamp` to ensure that subsequent events are
242
- // timestamped correctly.
243
- var index = chain_error_index;
244
- while (index > chain_start_index) {
245
- index -= 1;
246
-
247
- assert(index >= chain_start_index);
248
- assert(index < chain_error_index);
249
- const event = events[index];
250
- assert(event.timestamp <= self.commit_timestamp);
154
+ const callback = self.open_callback.?;
155
+ self.open_callback = null;
156
+ callback(self);
157
+ }
251
158
 
159
+ /// Returns the header's timestamp.
160
+ pub fn prepare(self: *StateMachine, operation: Operation, input: []u8) u64 {
252
161
  switch (operation) {
253
- .create_accounts => self.create_account_rollback(&event),
254
- .create_transfers => self.create_transfer_rollback(&event),
162
+ .root => unreachable,
163
+ .register => {},
164
+ .create_accounts => self.prepare_timestamps(.create_accounts, input),
165
+ .create_transfers => self.prepare_timestamps(.create_transfers, input),
166
+ .lookup_accounts => {},
167
+ .lookup_transfers => {},
255
168
  else => unreachable,
256
169
  }
257
- log.debug("{s} {}/{}: rollback(): {}", .{
258
- @tagName(operation),
259
- index + 1,
260
- events.len,
261
- event,
262
- });
170
+ return self.prepare_timestamp;
263
171
  }
264
- assert(index == chain_start_index);
265
- }
266
172
 
267
- fn execute_lookup_accounts(self: *StateMachine, input: []const u8, output: []u8) usize {
268
- const batch = mem.bytesAsSlice(u128, input);
269
- const output_len = @divFloor(output.len, @sizeOf(Account)) * @sizeOf(Account);
270
- const results = mem.bytesAsSlice(Account, output[0..output_len]);
271
- var results_count: usize = 0;
272
- for (batch) |id| {
273
- if (self.get_account(id)) |result| {
274
- results[results_count] = result.*;
275
- results_count += 1;
173
+ fn prepare_timestamps(
174
+ self: *StateMachine,
175
+ comptime operation: Operation,
176
+ input: []u8,
177
+ ) void {
178
+ var sum_reserved_timestamps: usize = 0;
179
+ var events = mem.bytesAsSlice(Event(operation), input);
180
+ for (events) |*event| {
181
+ sum_reserved_timestamps += event.timestamp;
182
+ self.prepare_timestamp += 1;
183
+ event.timestamp = self.prepare_timestamp;
276
184
  }
185
+ // The client is responsible for ensuring that timestamps are reserved:
186
+ // Use a single branch condition to detect non-zero reserved timestamps.
187
+ // Summing then branching once is faster than branching every iteration of the loop.
188
+ assert(sum_reserved_timestamps == 0);
277
189
  }
278
- return results_count * @sizeOf(Account);
279
- }
280
190
 
281
- fn execute_lookup_transfers(self: *StateMachine, input: []const u8, output: []u8) usize {
282
- const batch = mem.bytesAsSlice(u128, input);
283
- const output_len = @divFloor(output.len, @sizeOf(Transfer)) * @sizeOf(Transfer);
284
- const results = mem.bytesAsSlice(Transfer, output[0..output_len]);
285
- var results_count: usize = 0;
286
- for (batch) |id| {
287
- if (self.get_transfer(id)) |result| {
288
- results[results_count] = result.*;
289
- results_count += 1;
191
+ pub fn prefetch(
192
+ self: *StateMachine,
193
+ callback: fn (*StateMachine) void,
194
+ op: u64,
195
+ operation: Operation,
196
+ input: []align(16) const u8,
197
+ ) void {
198
+ _ = op;
199
+ assert(self.prefetch_input == null);
200
+ assert(self.prefetch_callback == null);
201
+
202
+ if (operation == .register) {
203
+ callback(self);
204
+ return;
290
205
  }
291
- }
292
- return results_count * @sizeOf(Transfer);
293
- }
294
206
 
295
- fn create_account(self: *StateMachine, a: *const Account) CreateAccountResult {
296
- assert(a.timestamp > self.commit_timestamp);
207
+ self.prefetch_input = input;
208
+ self.prefetch_callback = callback;
297
209
 
298
- if (a.flags.padding != 0) return .reserved_flag;
299
- if (!zeroed_48_bytes(a.reserved)) return .reserved_field;
210
+ // We do this here instead of at the end of commit() to avoid the need to call
211
+ // prefetch() in the StateMachine unit tests below.
212
+ self.forest.grooves.accounts.prefetch_clear();
213
+ self.forest.grooves.transfers.prefetch_clear();
214
+ self.forest.grooves.posted.prefetch_clear();
300
215
 
301
- if (a.id == 0) return .id_must_not_be_zero;
302
- if (a.ledger == 0) return .ledger_must_not_be_zero;
303
- if (a.code == 0) return .code_must_not_be_zero;
216
+ return switch (operation) {
217
+ .reserved, .root, .register => unreachable,
218
+ .create_accounts => {
219
+ self.prefetch_create_accounts(mem.bytesAsSlice(Account, input));
220
+ },
221
+ .create_transfers => {
222
+ self.prefetch_create_transfers(mem.bytesAsSlice(Transfer, input));
223
+ },
224
+ .lookup_accounts => {
225
+ self.prefetch_lookup_accounts(mem.bytesAsSlice(u128, input));
226
+ },
227
+ .lookup_transfers => {
228
+ self.prefetch_lookup_transfers(mem.bytesAsSlice(u128, input));
229
+ },
230
+ };
231
+ }
304
232
 
305
- if (a.flags.debits_must_not_exceed_credits and a.flags.credits_must_not_exceed_debits) {
306
- return .mutually_exclusive_flags;
233
+ fn prefetch_finish(self: *StateMachine) void {
234
+ assert(self.prefetch_input != null);
235
+ const callback = self.prefetch_callback.?;
236
+ self.prefetch_input = null;
237
+ self.prefetch_callback = null;
238
+ callback(self);
307
239
  }
308
240
 
309
- if (sum_overflows(a.debits_pending, a.debits_posted)) return .overflows_debits;
310
- if (sum_overflows(a.credits_pending, a.credits_posted)) return .overflows_credits;
241
+ fn prefetch_create_accounts(self: *StateMachine, accounts: []const Account) void {
242
+ for (accounts) |*a| {
243
+ self.forest.grooves.accounts.prefetch_enqueue(a.id);
244
+ }
245
+ self.forest.grooves.accounts.prefetch(
246
+ prefetch_create_accounts_callback,
247
+ &self.prefetch_accounts_context,
248
+ );
249
+ }
311
250
 
312
- // Opening balances may never exceed limits:
313
- if (a.debits_exceed_credits(0)) return .exceeds_credits;
314
- if (a.credits_exceed_debits(0)) return .exceeds_debits;
251
+ fn prefetch_create_accounts_callback(completion: *AccountsGroove.PrefetchContext) void {
252
+ const self = @fieldParentPtr(StateMachine, "prefetch_accounts_context", completion);
315
253
 
316
- if (self.get_account(a.id)) |e| return create_account_exists(a, e);
254
+ self.prefetch_finish();
255
+ }
317
256
 
318
- self.accounts.putAssumeCapacityNoClobber(a.id, a.*);
257
+ fn prefetch_create_transfers(self: *StateMachine, transfers: []const Transfer) void {
258
+ for (transfers) |*t| {
259
+ self.forest.grooves.transfers.prefetch_enqueue(t.id);
319
260
 
320
- self.commit_timestamp = a.timestamp;
321
- return .ok;
322
- }
261
+ if (t.flags.post_pending_transfer or t.flags.void_pending_transfer) {
262
+ self.forest.grooves.transfers.prefetch_enqueue(t.pending_id);
263
+ // This prefetch isn't run yet, but enqueue it here as well to save an extra
264
+ // iteration over transfers.
265
+ self.forest.grooves.posted.prefetch_enqueue(t.pending_id);
266
+ }
267
+ }
323
268
 
324
- fn create_account_rollback(self: *StateMachine, a: *const Account) void {
325
- assert(self.accounts.remove(a.id));
326
- }
269
+ self.forest.grooves.transfers.prefetch(
270
+ prefetch_create_transfers_callback_transfers,
271
+ &self.prefetch_transfers_context,
272
+ );
273
+ }
327
274
 
328
- fn create_account_exists(a: *const Account, e: *const Account) CreateAccountResult {
329
- assert(a.id == e.id);
330
- if (@bitCast(u16, a.flags) != @bitCast(u16, e.flags)) return .exists_with_different_flags;
331
- if (a.user_data != e.user_data) return .exists_with_different_user_data;
332
- assert(zeroed_48_bytes(a.reserved) and zeroed_48_bytes(e.reserved));
333
- if (a.ledger != e.ledger) return .exists_with_different_ledger;
334
- if (a.code != e.code) return .exists_with_different_code;
335
- if (a.debits_pending != e.debits_pending) return .exists_with_different_debits_pending;
336
- if (a.debits_posted != e.debits_posted) return .exists_with_different_debits_posted;
337
- if (a.credits_pending != e.credits_pending) return .exists_with_different_credits_pending;
338
- if (a.credits_posted != e.credits_posted) return .exists_with_different_credits_posted;
339
- return .exists;
340
- }
275
+ fn prefetch_create_transfers_callback_transfers(completion: *TransfersGroove.PrefetchContext) void {
276
+ const self = @fieldParentPtr(StateMachine, "prefetch_transfers_context", completion);
341
277
 
342
- fn create_transfer(self: *StateMachine, t: *const Transfer) CreateTransferResult {
343
- assert(t.timestamp > self.commit_timestamp);
278
+ const transfers = mem.bytesAsSlice(Event(.create_transfers), self.prefetch_input.?);
279
+ for (transfers) |*t| {
280
+ if (t.flags.post_pending_transfer or t.flags.void_pending_transfer) {
281
+ if (self.forest.grooves.transfers.get(t.pending_id)) |p| {
282
+ self.forest.grooves.accounts.prefetch_enqueue(p.debit_account_id);
283
+ self.forest.grooves.accounts.prefetch_enqueue(p.credit_account_id);
284
+ }
285
+ } else {
286
+ self.forest.grooves.accounts.prefetch_enqueue(t.debit_account_id);
287
+ self.forest.grooves.accounts.prefetch_enqueue(t.credit_account_id);
288
+ }
289
+ }
344
290
 
345
- if (t.flags.padding != 0) return .reserved_flag;
346
- if (t.reserved != 0) return .reserved_field;
291
+ self.forest.grooves.accounts.prefetch(
292
+ prefetch_create_transfers_callback_accounts,
293
+ &self.prefetch_accounts_context,
294
+ );
295
+ }
347
296
 
348
- if (t.id == 0) return .id_must_not_be_zero;
297
+ fn prefetch_create_transfers_callback_accounts(completion: *AccountsGroove.PrefetchContext) void {
298
+ const self = @fieldParentPtr(StateMachine, "prefetch_accounts_context", completion);
349
299
 
350
- if (t.flags.post_pending_transfer or t.flags.void_pending_transfer) {
351
- return self.post_or_void_pending_transfer(t);
300
+ self.forest.grooves.posted.prefetch(
301
+ prefetch_create_transfers_callback_posted,
302
+ &self.prefetch_posted_context,
303
+ );
352
304
  }
353
305
 
354
- if (t.debit_account_id == 0) return .debit_account_id_must_not_be_zero;
355
- if (t.credit_account_id == 0) return .credit_account_id_must_not_be_zero;
356
- if (t.credit_account_id == t.debit_account_id) return .accounts_must_be_different;
306
+ fn prefetch_create_transfers_callback_posted(completion: *PostedGroove.PrefetchContext) void {
307
+ const self = @fieldParentPtr(StateMachine, "prefetch_posted_context", completion);
357
308
 
358
- if (t.pending_id != 0) return .pending_id_must_be_zero;
359
- if (t.flags.pending) {
360
- // Otherwise, reserved amounts may never be released.
361
- if (t.timeout == 0) return .pending_transfer_must_timeout;
362
- } else {
363
- if (t.timeout != 0) return .timeout_reserved_for_pending_transfer;
309
+ self.prefetch_finish();
364
310
  }
365
311
 
366
- if (t.ledger == 0) return .ledger_must_not_be_zero;
367
- if (t.code == 0) return .code_must_not_be_zero;
368
- if (t.amount == 0) return .amount_must_not_be_zero;
369
-
370
- // The etymology of the DR and CR abbreviations for debit/credit is interesting, either:
371
- // 1. derived from the Latin past participles of debitum/creditum, i.e. debere/credere,
372
- // 2. standing for debit record and credit record, or
373
- // 3. relating to debtor and creditor.
374
- // We use them to distinguish between `cr` (credit account), and `c` (commit).
375
- const dr = self.get_account(t.debit_account_id) orelse return .debit_account_not_found;
376
- const cr = self.get_account(t.credit_account_id) orelse return .credit_account_not_found;
377
- assert(dr.id == t.debit_account_id);
378
- assert(cr.id == t.credit_account_id);
379
- assert(t.timestamp > dr.timestamp);
380
- assert(t.timestamp > cr.timestamp);
381
-
382
- if (dr.ledger != cr.ledger) return .accounts_must_have_the_same_ledger;
383
- if (t.ledger != dr.ledger) return .transfer_must_have_the_same_ledger_as_accounts;
384
-
385
- // If the transfer already exists, then it must not influence the overflow or limit checks.
386
- if (self.get_transfer(t.id)) |e| return create_transfer_exists(t, e);
387
-
388
- if (t.flags.pending) {
389
- if (sum_overflows(t.amount, dr.debits_pending)) return .overflows_debits_pending;
390
- if (sum_overflows(t.amount, cr.credits_pending)) return .overflows_credits_pending;
312
+ fn prefetch_lookup_accounts(self: *StateMachine, ids: []const u128) void {
313
+ for (ids) |id| {
314
+ self.forest.grooves.accounts.prefetch_enqueue(id);
315
+ }
316
+
317
+ self.forest.grooves.accounts.prefetch(
318
+ prefetch_lookup_accounts_callback,
319
+ &self.prefetch_accounts_context,
320
+ );
391
321
  }
392
- if (sum_overflows(t.amount, dr.debits_posted)) return .overflows_debits_posted;
393
- if (sum_overflows(t.amount, cr.credits_posted)) return .overflows_credits_posted;
394
- // We assert that the sum of the pending and posted balances can never overflow:
395
- if (sum_overflows(t.amount, dr.debits_pending + dr.debits_posted)) {
396
- return .overflows_debits;
322
+
323
+ fn prefetch_lookup_accounts_callback(completion: *AccountsGroove.PrefetchContext) void {
324
+ const self = @fieldParentPtr(StateMachine, "prefetch_accounts_context", completion);
325
+
326
+ self.prefetch_finish();
397
327
  }
398
- if (sum_overflows(t.amount, cr.credits_pending + cr.credits_posted)) {
399
- return .overflows_credits;
328
+
329
+ fn prefetch_lookup_transfers(self: *StateMachine, ids: []const u128) void {
330
+ for (ids) |id| {
331
+ self.forest.grooves.transfers.prefetch_enqueue(id);
332
+ }
333
+
334
+ self.forest.grooves.transfers.prefetch(
335
+ prefetch_lookup_transfers_callback,
336
+ &self.prefetch_transfers_context,
337
+ );
400
338
  }
401
339
 
402
- if (dr.debits_exceed_credits(t.amount)) return .exceeds_credits;
403
- if (cr.credits_exceed_debits(t.amount)) return .exceeds_debits;
340
+ fn prefetch_lookup_transfers_callback(completion: *TransfersGroove.PrefetchContext) void {
341
+ const self = @fieldParentPtr(StateMachine, "prefetch_transfers_context", completion);
404
342
 
405
- self.transfers.putAssumeCapacityNoClobber(t.id, t.*);
343
+ self.prefetch_finish();
344
+ }
406
345
 
407
- if (t.flags.pending) {
408
- dr.debits_pending += t.amount;
409
- cr.credits_pending += t.amount;
410
- } else {
411
- dr.debits_posted += t.amount;
412
- cr.credits_posted += t.amount;
346
+ pub fn commit(
347
+ self: *StateMachine,
348
+ client: u128,
349
+ op: u64,
350
+ operation: Operation,
351
+ input: []const u8,
352
+ output: []u8,
353
+ ) usize {
354
+ _ = client;
355
+ _ = op;
356
+
357
+ const result = switch (operation) {
358
+ .root => unreachable,
359
+ .register => 0,
360
+ .create_accounts => self.execute(.create_accounts, input, output),
361
+ .create_transfers => self.execute(.create_transfers, input, output),
362
+ .lookup_accounts => self.execute_lookup_accounts(input, output),
363
+ .lookup_transfers => self.execute_lookup_transfers(input, output),
364
+ else => unreachable,
365
+ };
366
+
367
+ return result;
413
368
  }
414
369
 
415
- self.commit_timestamp = t.timestamp;
416
- return .ok;
417
- }
370
+ pub fn tick(self: *StateMachine) void {
371
+ self.forest.tick();
372
+ }
373
+
374
+ pub fn compact(self: *StateMachine, callback: fn (*StateMachine) void, op: u64) void {
375
+ assert(self.compact_callback == null);
376
+ assert(self.checkpoint_callback == null);
418
377
 
419
- fn create_transfer_rollback(self: *StateMachine, t: *const Transfer) void {
420
- if (t.flags.post_pending_transfer or t.flags.void_pending_transfer) {
421
- return self.post_or_void_pending_transfer_rollback(t);
378
+ self.compact_callback = callback;
379
+ self.forest.compact(compact_finish, op);
422
380
  }
423
381
 
424
- const dr = self.get_account(t.debit_account_id).?;
425
- const cr = self.get_account(t.credit_account_id).?;
426
- assert(dr.id == t.debit_account_id);
427
- assert(cr.id == t.credit_account_id);
428
-
429
- if (t.flags.pending) {
430
- dr.debits_pending -= t.amount;
431
- cr.credits_pending -= t.amount;
432
- } else {
433
- dr.debits_posted -= t.amount;
434
- cr.credits_posted -= t.amount;
382
+ fn compact_finish(forest: *Forest) void {
383
+ const self = @fieldParentPtr(StateMachine, "forest", forest);
384
+ const callback = self.compact_callback.?;
385
+ self.compact_callback = null;
386
+ callback(self);
435
387
  }
436
- assert(self.transfers.remove(t.id));
437
- }
438
388
 
439
- fn create_transfer_exists(t: *const Transfer, e: *const Transfer) CreateTransferResult {
440
- assert(t.id == e.id);
441
- // The flags change the behavior of the remaining comparisons, so compare the flags first.
442
- if (@bitCast(u16, t.flags) != @bitCast(u16, e.flags)) return .exists_with_different_flags;
443
- if (t.debit_account_id != e.debit_account_id) {
444
- return .exists_with_different_debit_account_id;
389
+ pub fn checkpoint(self: *StateMachine, callback: fn (*StateMachine) void) void {
390
+ assert(self.compact_callback == null);
391
+ assert(self.checkpoint_callback == null);
392
+
393
+ self.checkpoint_callback = callback;
394
+ self.forest.checkpoint(checkpoint_finish);
445
395
  }
446
- if (t.credit_account_id != e.credit_account_id) {
447
- return .exists_with_different_credit_account_id;
396
+
397
+ fn checkpoint_finish(forest: *Forest) void {
398
+ const self = @fieldParentPtr(StateMachine, "forest", forest);
399
+ const callback = self.checkpoint_callback.?;
400
+ self.checkpoint_callback = null;
401
+ callback(self);
448
402
  }
449
- if (t.user_data != e.user_data) return .exists_with_different_user_data;
450
- assert(t.reserved == 0 and e.reserved == 0);
451
- assert(t.pending_id == 0 and e.pending_id == 0); // We know that the flags are the same.
452
- if (t.timeout != e.timeout) return .exists_with_different_timeout;
453
- assert(t.ledger == e.ledger); // If the accounts are the same, the ledger must be the same.
454
- if (t.code != e.code) return .exists_with_different_code;
455
- if (t.amount != e.amount) return .exists_with_different_amount;
456
- return .exists;
457
- }
458
403
 
459
- fn post_or_void_pending_transfer(self: *StateMachine, t: *const Transfer) CreateTransferResult {
460
- assert(t.id != 0);
461
- assert(t.flags.padding == 0);
462
- assert(t.reserved == 0);
463
- assert(t.timestamp > self.commit_timestamp);
464
- assert(t.flags.post_pending_transfer or t.flags.void_pending_transfer);
404
+ fn execute(
405
+ self: *StateMachine,
406
+ comptime operation: Operation,
407
+ input: []const u8,
408
+ output: []u8,
409
+ ) usize {
410
+ comptime assert(operation != .lookup_accounts and operation != .lookup_transfers);
411
+
412
+ const events = mem.bytesAsSlice(Event(operation), input);
413
+ var results = mem.bytesAsSlice(Result(operation), output);
414
+ var count: usize = 0;
415
+
416
+ var chain: ?usize = null;
417
+ var chain_broken = false;
418
+
419
+ for (events) |*event, index| {
420
+ if (event.flags.linked and chain == null) {
421
+ chain = index;
422
+ assert(chain_broken == false);
423
+ }
424
+ const result = if (chain_broken) .linked_event_failed else switch (operation) {
425
+ .create_accounts => self.create_account(event),
426
+ .create_transfers => self.create_transfer(event),
427
+ else => unreachable,
428
+ };
429
+ log.debug("{s} {}/{}: {}: {}", .{
430
+ @tagName(operation),
431
+ index + 1,
432
+ events.len,
433
+ result,
434
+ event,
435
+ });
436
+ if (result != .ok) {
437
+ if (chain) |chain_start_index| {
438
+ if (!chain_broken) {
439
+ chain_broken = true;
440
+ // Rollback events in LIFO order, excluding this event that broke the chain:
441
+ self.rollback(operation, input, chain_start_index, index);
442
+ // Add errors for rolled back events in FIFO order:
443
+ var chain_index = chain_start_index;
444
+ while (chain_index < index) : (chain_index += 1) {
445
+ results[count] = .{
446
+ .index = @intCast(u32, chain_index),
447
+ .result = .linked_event_failed,
448
+ };
449
+ count += 1;
450
+ }
451
+ } else {
452
+ assert(result == .linked_event_failed);
453
+ }
454
+ }
455
+ results[count] = .{ .index = @intCast(u32, index), .result = result };
456
+ count += 1;
457
+ }
458
+ if (!event.flags.linked and chain != null) {
459
+ chain = null;
460
+ chain_broken = false;
461
+ }
462
+ }
463
+ // TODO client.zig: Validate that batch chains are always well-formed and closed.
464
+ // This is programming error and we should raise an exception for this in the client ASAP.
465
+ assert(chain == null);
466
+ assert(chain_broken == false);
465
467
 
466
- if (t.flags.post_pending_transfer and t.flags.void_pending_transfer) {
467
- return .cannot_post_and_void_pending_transfer;
468
+ return @sizeOf(Result(operation)) * count;
468
469
  }
469
- if (t.flags.pending) return .pending_transfer_cannot_post_or_void_another;
470
- if (t.timeout != 0) return .timeout_reserved_for_pending_transfer;
471
-
472
- if (t.pending_id == 0) return .pending_id_must_not_be_zero;
473
- if (t.pending_id == t.id) return .pending_id_must_be_different;
474
-
475
- const p = self.get_transfer(t.pending_id) orelse return .pending_transfer_not_found;
476
- assert(p.id == t.pending_id);
477
- if (!p.flags.pending) return .pending_transfer_not_pending;
478
-
479
- const dr = self.get_account(p.debit_account_id).?;
480
- const cr = self.get_account(p.credit_account_id).?;
481
- assert(dr.id == p.debit_account_id);
482
- assert(cr.id == p.credit_account_id);
483
- assert(p.timestamp > dr.timestamp);
484
- assert(p.timestamp > cr.timestamp);
485
- assert(p.amount > 0);
486
-
487
- if (t.debit_account_id > 0 and t.debit_account_id != p.debit_account_id) {
488
- return .pending_transfer_has_different_debit_account_id;
470
+
471
+ fn rollback(
472
+ self: *StateMachine,
473
+ comptime operation: Operation,
474
+ input: []const u8,
475
+ chain_start_index: usize,
476
+ chain_error_index: usize,
477
+ ) void {
478
+ const events = mem.bytesAsSlice(Event(operation), input);
479
+
480
+ // We commit events in FIFO order.
481
+ // We must therefore rollback events in LIFO order with a reverse loop.
482
+ // We do not rollback `self.commit_timestamp` to ensure that subsequent events are
483
+ // timestamped correctly.
484
+ var index = chain_error_index;
485
+ while (index > chain_start_index) {
486
+ index -= 1;
487
+
488
+ assert(index >= chain_start_index);
489
+ assert(index < chain_error_index);
490
+ const event = events[index];
491
+ assert(event.timestamp <= self.commit_timestamp);
492
+
493
+ switch (operation) {
494
+ .create_accounts => self.create_account_rollback(&event),
495
+ .create_transfers => self.create_transfer_rollback(&event),
496
+ else => unreachable,
497
+ }
498
+ log.debug("{s} {}/{}: rollback(): {}", .{
499
+ @tagName(operation),
500
+ index + 1,
501
+ events.len,
502
+ event,
503
+ });
504
+ }
505
+ assert(index == chain_start_index);
506
+ }
507
+
508
+ fn execute_lookup_accounts(self: *StateMachine, input: []const u8, output: []u8) usize {
509
+ const batch = mem.bytesAsSlice(u128, input);
510
+ const output_len = @divFloor(output.len, @sizeOf(Account)) * @sizeOf(Account);
511
+ const results = mem.bytesAsSlice(Account, output[0..output_len]);
512
+ var results_count: usize = 0;
513
+ for (batch) |id| {
514
+ if (self.get_account(id)) |result| {
515
+ results[results_count] = result.*;
516
+ results_count += 1;
517
+ }
518
+ }
519
+ return results_count * @sizeOf(Account);
489
520
  }
490
- if (t.credit_account_id > 0 and t.credit_account_id != p.credit_account_id) {
491
- return .pending_transfer_has_different_credit_account_id;
521
+
522
+ fn execute_lookup_transfers(self: *StateMachine, input: []const u8, output: []u8) usize {
523
+ const batch = mem.bytesAsSlice(u128, input);
524
+ const output_len = @divFloor(output.len, @sizeOf(Transfer)) * @sizeOf(Transfer);
525
+ const results = mem.bytesAsSlice(Transfer, output[0..output_len]);
526
+ var results_count: usize = 0;
527
+ for (batch) |id| {
528
+ if (self.get_transfer(id)) |result| {
529
+ results[results_count] = result.*;
530
+ results_count += 1;
531
+ }
532
+ }
533
+ return results_count * @sizeOf(Transfer);
492
534
  }
493
- // The user_data field is allowed to differ across pending and posting/voiding transfers.
494
- if (t.ledger > 0 and t.ledger != p.ledger) return .pending_transfer_has_different_ledger;
495
- if (t.code > 0 and t.code != p.code) return .pending_transfer_has_different_code;
496
535
 
497
- const amount = if (t.amount > 0) t.amount else p.amount;
498
- if (amount > p.amount) return .exceeds_pending_transfer_amount;
536
+ fn create_account(self: *StateMachine, a: *const Account) CreateAccountResult {
537
+ assert(a.timestamp > self.commit_timestamp);
538
+
539
+ if (a.flags.padding != 0) return .reserved_flag;
540
+ if (!zeroed_48_bytes(a.reserved)) return .reserved_field;
541
+
542
+ if (a.id == 0) return .id_must_not_be_zero;
543
+ if (a.id == math.maxInt(u128)) return .id_must_not_be_int_max;
544
+ if (a.ledger == 0) return .ledger_must_not_be_zero;
545
+ if (a.code == 0) return .code_must_not_be_zero;
546
+
547
+ if (a.flags.debits_must_not_exceed_credits and a.flags.credits_must_not_exceed_debits) {
548
+ return .mutually_exclusive_flags;
549
+ }
550
+
551
+ if (sum_overflows(a.debits_pending, a.debits_posted)) return .overflows_debits;
552
+ if (sum_overflows(a.credits_pending, a.credits_posted)) return .overflows_credits;
553
+
554
+ // Opening balances may never exceed limits:
555
+ if (a.debits_exceed_credits(0)) return .exceeds_credits;
556
+ if (a.credits_exceed_debits(0)) return .exceeds_debits;
557
+
558
+ if (self.get_account(a.id)) |e| return create_account_exists(a, e);
559
+
560
+ self.forest.grooves.accounts.put(a);
499
561
 
500
- if (t.flags.void_pending_transfer and amount < p.amount) {
501
- return .pending_transfer_has_different_amount;
562
+ self.commit_timestamp = a.timestamp;
563
+ return .ok;
502
564
  }
503
565
 
504
- if (self.get_transfer(t.id)) |e| return post_or_void_pending_transfer_exists(t, e, p);
566
+ fn create_account_rollback(self: *StateMachine, a: *const Account) void {
567
+ self.forest.grooves.accounts.remove(a.id);
568
+ }
505
569
 
506
- if (self.get_posted(t.pending_id)) |posted| {
507
- if (posted) return .pending_transfer_already_posted;
508
- return .pending_transfer_already_voided;
570
+ fn create_account_exists(a: *const Account, e: *const Account) CreateAccountResult {
571
+ assert(a.id == e.id);
572
+ if (@bitCast(u16, a.flags) != @bitCast(u16, e.flags)) return .exists_with_different_flags;
573
+ if (a.user_data != e.user_data) return .exists_with_different_user_data;
574
+ assert(zeroed_48_bytes(a.reserved) and zeroed_48_bytes(e.reserved));
575
+ if (a.ledger != e.ledger) return .exists_with_different_ledger;
576
+ if (a.code != e.code) return .exists_with_different_code;
577
+ if (a.debits_pending != e.debits_pending) return .exists_with_different_debits_pending;
578
+ if (a.debits_posted != e.debits_posted) return .exists_with_different_debits_posted;
579
+ if (a.credits_pending != e.credits_pending) return .exists_with_different_credits_pending;
580
+ if (a.credits_posted != e.credits_posted) return .exists_with_different_credits_posted;
581
+ return .exists;
509
582
  }
510
583
 
511
- assert(p.timestamp < t.timestamp);
512
- assert(p.timeout > 0);
513
- if (p.timestamp + p.timeout <= t.timestamp) return .pending_transfer_expired;
514
-
515
- self.transfers.putAssumeCapacityNoClobber(t.id, .{
516
- .id = t.id,
517
- .debit_account_id = p.debit_account_id,
518
- .credit_account_id = p.credit_account_id,
519
- .user_data = if (t.user_data > 0) t.user_data else p.user_data,
520
- .reserved = p.reserved,
521
- .ledger = p.ledger,
522
- .code = p.code,
523
- .pending_id = t.pending_id,
524
- .timeout = t.timeout,
525
- .timestamp = t.timestamp,
526
- .flags = t.flags,
527
- .amount = amount,
528
- });
584
+ fn create_transfer(self: *StateMachine, t: *const Transfer) CreateTransferResult {
585
+ assert(t.timestamp > self.commit_timestamp);
529
586
 
530
- self.posted.putAssumeCapacityNoClobber(t.pending_id, t.flags.post_pending_transfer);
587
+ if (t.flags.padding != 0) return .reserved_flag;
588
+ if (t.reserved != 0) return .reserved_field;
531
589
 
532
- dr.debits_pending -= p.amount;
533
- cr.credits_pending -= p.amount;
590
+ if (t.id == 0) return .id_must_not_be_zero;
591
+ if (t.id == math.maxInt(u128)) return .id_must_not_be_int_max;
534
592
 
535
- if (t.flags.post_pending_transfer) {
536
- assert(amount > 0);
537
- assert(amount <= p.amount);
538
- dr.debits_posted += amount;
539
- cr.credits_posted += amount;
540
- }
593
+ if (t.flags.post_pending_transfer or t.flags.void_pending_transfer) {
594
+ return self.post_or_void_pending_transfer(t);
595
+ }
541
596
 
542
- self.commit_timestamp = t.timestamp;
543
- return .ok;
544
- }
597
+ if (t.debit_account_id == 0) return .debit_account_id_must_not_be_zero;
598
+ if (t.debit_account_id == math.maxInt(u128)) return .debit_account_id_must_not_be_int_max;
599
+ if (t.credit_account_id == 0) return .credit_account_id_must_not_be_zero;
600
+ if (t.credit_account_id == math.maxInt(u128)) return .credit_account_id_must_not_be_int_max;
601
+ if (t.credit_account_id == t.debit_account_id) return .accounts_must_be_different;
602
+
603
+ if (t.pending_id != 0) return .pending_id_must_be_zero;
604
+ if (t.flags.pending) {
605
+ // Otherwise, reserved amounts may never be released.
606
+ if (t.timeout == 0) return .pending_transfer_must_timeout;
607
+ } else {
608
+ if (t.timeout != 0) return .timeout_reserved_for_pending_transfer;
609
+ }
610
+
611
+ if (t.ledger == 0) return .ledger_must_not_be_zero;
612
+ if (t.code == 0) return .code_must_not_be_zero;
613
+ if (t.amount == 0) return .amount_must_not_be_zero;
614
+
615
+ // The etymology of the DR and CR abbreviations for debit/credit is interesting, either:
616
+ // 1. derived from the Latin past participles of debitum/creditum, i.e. debere/credere,
617
+ // 2. standing for debit record and credit record, or
618
+ // 3. relating to debtor and creditor.
619
+ // We use them to distinguish between `cr` (credit account), and `c` (commit).
620
+ const dr = self.get_account(t.debit_account_id) orelse return .debit_account_not_found;
621
+ const cr = self.get_account(t.credit_account_id) orelse return .credit_account_not_found;
622
+ assert(dr.id == t.debit_account_id);
623
+ assert(cr.id == t.credit_account_id);
624
+ assert(t.timestamp > dr.timestamp);
625
+ assert(t.timestamp > cr.timestamp);
626
+
627
+ if (dr.ledger != cr.ledger) return .accounts_must_have_the_same_ledger;
628
+ if (t.ledger != dr.ledger) return .transfer_must_have_the_same_ledger_as_accounts;
629
+
630
+ // If the transfer already exists, then it must not influence the overflow or limit checks.
631
+ if (self.get_transfer(t.id)) |e| return create_transfer_exists(t, e);
632
+
633
+ if (t.flags.pending) {
634
+ if (sum_overflows(t.amount, dr.debits_pending)) return .overflows_debits_pending;
635
+ if (sum_overflows(t.amount, cr.credits_pending)) return .overflows_credits_pending;
636
+ }
637
+ if (sum_overflows(t.amount, dr.debits_posted)) return .overflows_debits_posted;
638
+ if (sum_overflows(t.amount, cr.credits_posted)) return .overflows_credits_posted;
639
+ // We assert that the sum of the pending and posted balances can never overflow:
640
+ if (sum_overflows(t.amount, dr.debits_pending + dr.debits_posted)) {
641
+ return .overflows_debits;
642
+ }
643
+ if (sum_overflows(t.amount, cr.credits_pending + cr.credits_posted)) {
644
+ return .overflows_credits;
645
+ }
545
646
 
546
- fn post_or_void_pending_transfer_rollback(self: *StateMachine, t: *const Transfer) void {
547
- assert(t.id > 0);
548
- assert(t.flags.post_pending_transfer or t.flags.void_pending_transfer);
647
+ if (dr.debits_exceed_credits(t.amount)) return .exceeds_credits;
648
+ if (cr.credits_exceed_debits(t.amount)) return .exceeds_debits;
549
649
 
550
- assert(t.pending_id > 0);
551
- const p = self.get_transfer(t.pending_id).?;
552
- assert(p.id == t.pending_id);
553
- assert(p.debit_account_id > 0);
554
- assert(p.credit_account_id > 0);
650
+ self.forest.grooves.transfers.put_no_clobber(t);
555
651
 
556
- const dr = self.get_account(p.debit_account_id).?;
557
- const cr = self.get_account(p.credit_account_id).?;
558
- assert(dr.id == p.debit_account_id);
559
- assert(cr.id == p.credit_account_id);
652
+ var dr_new = dr.*;
653
+ var cr_new = cr.*;
654
+ if (t.flags.pending) {
655
+ dr_new.debits_pending += t.amount;
656
+ cr_new.credits_pending += t.amount;
657
+ } else {
658
+ dr_new.debits_posted += t.amount;
659
+ cr_new.credits_posted += t.amount;
660
+ }
661
+ self.forest.grooves.accounts.put(&dr_new);
662
+ self.forest.grooves.accounts.put(&cr_new);
560
663
 
561
- if (t.flags.post_pending_transfer) {
562
- const amount = if (t.amount > 0) t.amount else p.amount;
563
- assert(amount > 0);
564
- assert(amount <= p.amount);
565
- dr.debits_posted -= amount;
566
- cr.credits_posted -= amount;
664
+ self.commit_timestamp = t.timestamp;
665
+ return .ok;
567
666
  }
568
- dr.debits_pending += p.amount;
569
- cr.credits_pending += p.amount;
570
667
 
571
- assert(self.posted.remove(t.pending_id));
572
- assert(self.transfers.remove(t.id));
573
- }
668
+ fn create_transfer_rollback(self: *StateMachine, t: *const Transfer) void {
669
+ if (t.flags.post_pending_transfer or t.flags.void_pending_transfer) {
670
+ return self.post_or_void_pending_transfer_rollback(t);
671
+ }
672
+
673
+ var dr = self.get_account(t.debit_account_id).?.*;
674
+ var cr = self.get_account(t.credit_account_id).?.*;
675
+ assert(dr.id == t.debit_account_id);
676
+ assert(cr.id == t.credit_account_id);
677
+
678
+ if (t.flags.pending) {
679
+ dr.debits_pending -= t.amount;
680
+ cr.credits_pending -= t.amount;
681
+ } else {
682
+ dr.debits_posted -= t.amount;
683
+ cr.credits_posted -= t.amount;
684
+ }
685
+ self.forest.grooves.accounts.put(&dr);
686
+ self.forest.grooves.accounts.put(&cr);
574
687
 
575
- fn post_or_void_pending_transfer_exists(
576
- t: *const Transfer,
577
- e: *const Transfer,
578
- p: *const Transfer,
579
- ) CreateTransferResult {
580
- assert(p.flags.pending);
581
- assert(t.pending_id == p.id);
582
- assert(t.id != p.id);
583
- assert(t.id == e.id);
584
-
585
- // Do not assume that `e` is necessarily a posting or voiding transfer.
586
- if (@bitCast(u16, t.flags) != @bitCast(u16, e.flags)) {
587
- return .exists_with_different_flags;
688
+ self.forest.grooves.transfers.remove(t.id);
588
689
  }
589
690
 
590
- // If `e` posted or voided a different pending transfer, then the accounts will differ.
591
- if (t.pending_id != e.pending_id) return .exists_with_different_pending_id;
592
-
593
- assert(e.flags.post_pending_transfer or e.flags.void_pending_transfer);
594
- assert(e.debit_account_id == p.debit_account_id);
595
- assert(e.credit_account_id == p.credit_account_id);
596
- assert(e.reserved == 0);
597
- assert(e.pending_id == p.id);
598
- assert(e.timeout == 0);
599
- assert(e.ledger == p.ledger);
600
- assert(e.code == p.code);
601
- assert(e.timestamp > p.timestamp);
602
-
603
- assert(t.flags.post_pending_transfer == e.flags.post_pending_transfer);
604
- assert(t.flags.void_pending_transfer == e.flags.void_pending_transfer);
605
- assert(t.debit_account_id == 0 or t.debit_account_id == e.debit_account_id);
606
- assert(t.credit_account_id == 0 or t.credit_account_id == e.credit_account_id);
607
- assert(t.reserved == 0);
608
- assert(t.timeout == 0);
609
- assert(t.ledger == 0 or t.ledger == e.ledger);
610
- assert(t.code == 0 or t.code == e.code);
611
- assert(t.timestamp > e.timestamp);
612
-
613
- if (t.user_data == 0) {
614
- if (e.user_data != p.user_data) return .exists_with_different_user_data;
615
- } else {
691
+ fn create_transfer_exists(t: *const Transfer, e: *const Transfer) CreateTransferResult {
692
+ assert(t.id == e.id);
693
+ // The flags change the behavior of the remaining comparisons, so compare the flags first.
694
+ if (@bitCast(u16, t.flags) != @bitCast(u16, e.flags)) return .exists_with_different_flags;
695
+ if (t.debit_account_id != e.debit_account_id) {
696
+ return .exists_with_different_debit_account_id;
697
+ }
698
+ if (t.credit_account_id != e.credit_account_id) {
699
+ return .exists_with_different_credit_account_id;
700
+ }
616
701
  if (t.user_data != e.user_data) return .exists_with_different_user_data;
702
+ assert(t.reserved == 0 and e.reserved == 0);
703
+ assert(t.pending_id == 0 and e.pending_id == 0); // We know that the flags are the same.
704
+ if (t.timeout != e.timeout) return .exists_with_different_timeout;
705
+ assert(t.ledger == e.ledger); // If the accounts are the same, the ledger must be the same.
706
+ if (t.code != e.code) return .exists_with_different_code;
707
+ if (t.amount != e.amount) return .exists_with_different_amount;
708
+ return .exists;
617
709
  }
618
710
 
619
- if (t.amount == 0) {
620
- if (e.amount != p.amount) return .exists_with_different_amount;
621
- } else {
622
- if (t.amount != e.amount) return .exists_with_different_amount;
711
+ fn post_or_void_pending_transfer(self: *StateMachine, t: *const Transfer) CreateTransferResult {
712
+ assert(t.id != 0);
713
+ assert(t.flags.padding == 0);
714
+ assert(t.reserved == 0);
715
+ assert(t.timestamp > self.commit_timestamp);
716
+ assert(t.flags.post_pending_transfer or t.flags.void_pending_transfer);
717
+
718
+ if (t.flags.post_pending_transfer and t.flags.void_pending_transfer) {
719
+ return .cannot_post_and_void_pending_transfer;
720
+ }
721
+ if (t.flags.pending) return .pending_transfer_cannot_post_or_void_another;
722
+ if (t.timeout != 0) return .timeout_reserved_for_pending_transfer;
723
+
724
+ if (t.pending_id == 0) return .pending_id_must_not_be_zero;
725
+ if (t.pending_id == math.maxInt(u128)) return .pending_id_must_not_be_int_max;
726
+ if (t.pending_id == t.id) return .pending_id_must_be_different;
727
+
728
+ const p = self.get_transfer(t.pending_id) orelse return .pending_transfer_not_found;
729
+ assert(p.id == t.pending_id);
730
+ if (!p.flags.pending) return .pending_transfer_not_pending;
731
+
732
+ const dr = self.get_account(p.debit_account_id).?;
733
+ const cr = self.get_account(p.credit_account_id).?;
734
+ assert(dr.id == p.debit_account_id);
735
+ assert(cr.id == p.credit_account_id);
736
+ assert(p.timestamp > dr.timestamp);
737
+ assert(p.timestamp > cr.timestamp);
738
+ assert(p.amount > 0);
739
+
740
+ if (t.debit_account_id > 0 and t.debit_account_id != p.debit_account_id) {
741
+ return .pending_transfer_has_different_debit_account_id;
742
+ }
743
+ if (t.credit_account_id > 0 and t.credit_account_id != p.credit_account_id) {
744
+ return .pending_transfer_has_different_credit_account_id;
745
+ }
746
+ // The user_data field is allowed to differ across pending and posting/voiding transfers.
747
+ if (t.ledger > 0 and t.ledger != p.ledger) return .pending_transfer_has_different_ledger;
748
+ if (t.code > 0 and t.code != p.code) return .pending_transfer_has_different_code;
749
+
750
+ const amount = if (t.amount > 0) t.amount else p.amount;
751
+ if (amount > p.amount) return .exceeds_pending_transfer_amount;
752
+
753
+ if (t.flags.void_pending_transfer and amount < p.amount) {
754
+ return .pending_transfer_has_different_amount;
755
+ }
756
+
757
+ if (self.get_transfer(t.id)) |e| return post_or_void_pending_transfer_exists(t, e, p);
758
+
759
+ if (self.get_posted(t.pending_id)) |posted| {
760
+ if (posted) return .pending_transfer_already_posted;
761
+ return .pending_transfer_already_voided;
762
+ }
763
+
764
+ assert(p.timestamp < t.timestamp);
765
+ assert(p.timeout > 0);
766
+ if (p.timestamp + p.timeout <= t.timestamp) return .pending_transfer_expired;
767
+
768
+ self.forest.grooves.transfers.put_no_clobber(&Transfer{
769
+ .id = t.id,
770
+ .debit_account_id = p.debit_account_id,
771
+ .credit_account_id = p.credit_account_id,
772
+ .user_data = if (t.user_data > 0) t.user_data else p.user_data,
773
+ .reserved = p.reserved,
774
+ .ledger = p.ledger,
775
+ .code = p.code,
776
+ .pending_id = t.pending_id,
777
+ .timeout = t.timeout,
778
+ .timestamp = t.timestamp,
779
+ .flags = t.flags,
780
+ .amount = amount,
781
+ });
782
+
783
+ self.forest.grooves.posted.put_no_clobber(t.pending_id, t.flags.post_pending_transfer);
784
+
785
+ var dr_new = dr.*;
786
+ var cr_new = cr.*;
787
+
788
+ dr_new.debits_pending -= p.amount;
789
+ cr_new.credits_pending -= p.amount;
790
+
791
+ if (t.flags.post_pending_transfer) {
792
+ assert(amount > 0);
793
+ assert(amount <= p.amount);
794
+ dr_new.debits_posted += amount;
795
+ cr_new.credits_posted += amount;
796
+ }
797
+
798
+ self.forest.grooves.accounts.put(&dr_new);
799
+ self.forest.grooves.accounts.put(&cr_new);
800
+
801
+ self.commit_timestamp = t.timestamp;
802
+ return .ok;
623
803
  }
624
804
 
625
- return .exists;
626
- }
805
+ fn post_or_void_pending_transfer_rollback(self: *StateMachine, t: *const Transfer) void {
806
+ assert(t.id > 0);
807
+ assert(t.flags.post_pending_transfer or t.flags.void_pending_transfer);
808
+
809
+ assert(t.pending_id > 0);
810
+ const p = self.get_transfer(t.pending_id).?;
811
+ assert(p.id == t.pending_id);
812
+ assert(p.debit_account_id > 0);
813
+ assert(p.credit_account_id > 0);
814
+
815
+ var dr = self.get_account(p.debit_account_id).?.*;
816
+ var cr = self.get_account(p.credit_account_id).?.*;
817
+ assert(dr.id == p.debit_account_id);
818
+ assert(cr.id == p.credit_account_id);
819
+
820
+ if (t.flags.post_pending_transfer) {
821
+ const amount = if (t.amount > 0) t.amount else p.amount;
822
+ assert(amount > 0);
823
+ assert(amount <= p.amount);
824
+ dr.debits_posted -= amount;
825
+ cr.credits_posted -= amount;
826
+ }
827
+ dr.debits_pending += p.amount;
828
+ cr.credits_pending += p.amount;
627
829
 
628
- /// This is our core private method for changing balances.
629
- /// Returns a live pointer to an Account in the accounts hash map.
630
- /// This is intended to lookup an Account and modify balances directly by reference.
631
- /// This pointer is invalidated if the hash map is resized by another insert, e.g. if we get a
632
- /// pointer, insert another account without capacity, and then modify this pointer... BOOM!
633
- /// This is a sharp tool but replaces a lookup, copy and update with a single lookup.
634
- fn get_account(self: *const StateMachine, id: u128) ?*Account {
635
- return self.accounts.getPtr(id);
636
- }
830
+ self.forest.grooves.accounts.put(&dr);
831
+ self.forest.grooves.accounts.put(&cr);
637
832
 
638
- /// See the comment for get_account().
639
- fn get_transfer(self: *const StateMachine, id: u128) ?*Transfer {
640
- return self.transfers.getPtr(id);
641
- }
833
+ self.forest.grooves.posted.remove(t.pending_id);
834
+ self.forest.grooves.transfers.remove(t.id);
835
+ }
642
836
 
643
- /// Returns whether a pending transfer, if it exists, has already been posted or voided.
644
- fn get_posted(self: *const StateMachine, pending_id: u128) ?bool {
645
- return self.posted.get(pending_id);
646
- }
647
- };
837
+ fn post_or_void_pending_transfer_exists(
838
+ t: *const Transfer,
839
+ e: *const Transfer,
840
+ p: *const Transfer,
841
+ ) CreateTransferResult {
842
+ assert(p.flags.pending);
843
+ assert(t.pending_id == p.id);
844
+ assert(t.id != p.id);
845
+ assert(t.id == e.id);
846
+
847
+ // Do not assume that `e` is necessarily a posting or voiding transfer.
848
+ if (@bitCast(u16, t.flags) != @bitCast(u16, e.flags)) {
849
+ return .exists_with_different_flags;
850
+ }
851
+
852
+ // If `e` posted or voided a different pending transfer, then the accounts will differ.
853
+ if (t.pending_id != e.pending_id) return .exists_with_different_pending_id;
854
+
855
+ assert(e.flags.post_pending_transfer or e.flags.void_pending_transfer);
856
+ assert(e.debit_account_id == p.debit_account_id);
857
+ assert(e.credit_account_id == p.credit_account_id);
858
+ assert(e.reserved == 0);
859
+ assert(e.pending_id == p.id);
860
+ assert(e.timeout == 0);
861
+ assert(e.ledger == p.ledger);
862
+ assert(e.code == p.code);
863
+ assert(e.timestamp > p.timestamp);
864
+
865
+ assert(t.flags.post_pending_transfer == e.flags.post_pending_transfer);
866
+ assert(t.flags.void_pending_transfer == e.flags.void_pending_transfer);
867
+ assert(t.debit_account_id == 0 or t.debit_account_id == e.debit_account_id);
868
+ assert(t.credit_account_id == 0 or t.credit_account_id == e.credit_account_id);
869
+ assert(t.reserved == 0);
870
+ assert(t.timeout == 0);
871
+ assert(t.ledger == 0 or t.ledger == e.ledger);
872
+ assert(t.code == 0 or t.code == e.code);
873
+ assert(t.timestamp > e.timestamp);
874
+
875
+ if (t.user_data == 0) {
876
+ if (e.user_data != p.user_data) return .exists_with_different_user_data;
877
+ } else {
878
+ if (t.user_data != e.user_data) return .exists_with_different_user_data;
879
+ }
880
+
881
+ if (t.amount == 0) {
882
+ if (e.amount != p.amount) return .exists_with_different_amount;
883
+ } else {
884
+ if (t.amount != e.amount) return .exists_with_different_amount;
885
+ }
886
+
887
+ return .exists;
888
+ }
889
+
890
+ fn get_account(self: *const StateMachine, id: u128) ?*const Account {
891
+ return self.forest.grooves.accounts.get(id);
892
+ }
893
+
894
+ fn get_transfer(self: *const StateMachine, id: u128) ?*const Transfer {
895
+ return self.forest.grooves.transfers.get(id);
896
+ }
897
+
898
+ /// Returns whether a pending transfer, if it exists, has already been posted or voided.
899
+ fn get_posted(self: *const StateMachine, pending_id: u128) ?bool {
900
+ return self.forest.grooves.posted.get(pending_id);
901
+ }
902
+ };
903
+ }
648
904
 
649
905
  fn sum_overflows(a: u64, b: u64) bool {
650
906
  var c: u64 = undefined;
@@ -701,6 +957,62 @@ test "sum_overflows" {
701
957
  try expectEqual(true, sum_overflows(math.maxInt(u64), math.maxInt(u64)));
702
958
  }
703
959
 
960
+ const TestContext = struct {
961
+ const Storage = @import("test/storage.zig").Storage;
962
+ const MessagePool = @import("message_pool.zig").MessagePool;
963
+ const SuperBlock = @import("vsr/superblock.zig").SuperBlockType(Storage);
964
+ const Grid = @import("lsm/grid.zig").GridType(Storage);
965
+ const StateMachine = StateMachineType(Storage);
966
+
967
+ storage: Storage,
968
+ message_pool: MessagePool,
969
+ superblock: SuperBlock,
970
+ grid: Grid,
971
+ state_machine: StateMachine,
972
+
973
+ fn init(ctx: *TestContext, allocator: mem.Allocator, options: StateMachine.Options) !void {
974
+ ctx.storage = try Storage.init(
975
+ allocator,
976
+ 4096,
977
+ .{
978
+ .seed = 0,
979
+ .read_latency_min = 0,
980
+ .read_latency_mean = 0,
981
+ .write_latency_min = 0,
982
+ .write_latency_mean = 0,
983
+ .read_fault_probability = 0,
984
+ .write_fault_probability = 0,
985
+ },
986
+ 0,
987
+ .{
988
+ .first_offset = 0,
989
+ .period = 0,
990
+ },
991
+ );
992
+ errdefer ctx.storage.deinit(allocator);
993
+
994
+ ctx.message_pool = .{ .free_list = null };
995
+
996
+ ctx.superblock = try SuperBlock.init(allocator, &ctx.storage, &ctx.message_pool);
997
+ errdefer ctx.superblock.deinit(allocator);
998
+
999
+ ctx.grid = try Grid.init(allocator, &ctx.superblock);
1000
+ errdefer ctx.grid.deinit(allocator);
1001
+
1002
+ ctx.state_machine = try StateMachine.init(allocator, &ctx.grid, options);
1003
+ errdefer ctx.state_machine.deinit(allocator);
1004
+ }
1005
+
1006
+ fn deinit(ctx: *TestContext, allocator: mem.Allocator) void {
1007
+ ctx.storage.deinit(allocator);
1008
+ ctx.superblock.deinit(allocator);
1009
+ ctx.grid.deinit(allocator);
1010
+ ctx.state_machine.deinit(allocator);
1011
+ ctx.message_pool.deinit(allocator);
1012
+ ctx.* = undefined;
1013
+ }
1014
+ };
1015
+
704
1016
  test "create/lookup/rollback accounts" {
705
1017
  const Vector = struct { result: CreateAccountResult, object: Account };
706
1018
 
@@ -778,6 +1090,25 @@ test "create/lookup/rollback accounts" {
778
1090
  .timestamp = 2,
779
1091
  }),
780
1092
  },
1093
+ .{
1094
+ .result = .id_must_not_be_int_max,
1095
+ .object = mem.zeroInit(Account, .{
1096
+ .id = math.maxInt(u128),
1097
+ .user_data = 0,
1098
+ .ledger = 0,
1099
+ .code = 0,
1100
+ .flags = .{
1101
+ .padding = 0,
1102
+ .debits_must_not_exceed_credits = true,
1103
+ .credits_must_not_exceed_debits = true,
1104
+ },
1105
+ .debits_pending = math.maxInt(u64),
1106
+ .debits_posted = math.maxInt(u64),
1107
+ .credits_pending = math.maxInt(u64),
1108
+ .credits_posted = math.maxInt(u64),
1109
+ .timestamp = 2,
1110
+ }),
1111
+ },
781
1112
  .{
782
1113
  .result = .ledger_must_not_be_zero,
783
1114
  .object = mem.zeroInit(Account, .{
@@ -1032,10 +1363,18 @@ test "create/lookup/rollback accounts" {
1032
1363
  },
1033
1364
  };
1034
1365
 
1035
- var state_machine = try StateMachine.init(testing.allocator, vectors.len, 0, 0);
1036
- defer state_machine.deinit();
1366
+ var context: TestContext = undefined;
1367
+ try context.init(testing.allocator, .{
1368
+ .lsm_forest_node_count = 1,
1369
+ .cache_size_accounts = vectors.len,
1370
+ .cache_size_transfers = 0,
1371
+ .cache_size_posted = 0,
1372
+ });
1373
+ defer context.deinit(testing.allocator);
1037
1374
 
1038
- for (vectors) |vector, i| {
1375
+ const state_machine = &context.state_machine;
1376
+
1377
+ for (vectors) |*vector, i| {
1039
1378
  const result = state_machine.create_account(&vector.object);
1040
1379
  expectEqual(vector.result, result) catch |err| {
1041
1380
  print_test_vector(i, vector.result, result, vector.object, err);
@@ -1090,21 +1429,24 @@ test "linked accounts" {
1090
1429
  mem.zeroInit(Account, .{ .id = 4, .code = 1, .ledger = 1 }),
1091
1430
  };
1092
1431
 
1093
- var state_machine = try StateMachine.init(
1094
- testing.allocator,
1095
- accounts_max,
1096
- transfers_max,
1097
- transfers_pending_max,
1098
- );
1099
- defer state_machine.deinit();
1432
+ var context: TestContext = undefined;
1433
+ try context.init(testing.allocator, .{
1434
+ .lsm_forest_node_count = 1,
1435
+ .cache_size_accounts = accounts_max,
1436
+ .cache_size_transfers = transfers_max,
1437
+ .cache_size_posted = transfers_pending_max,
1438
+ });
1439
+ defer context.deinit(testing.allocator);
1440
+
1441
+ const state_machine = &context.state_machine;
1100
1442
 
1101
1443
  const input = mem.asBytes(&accounts);
1102
1444
 
1103
1445
  const output = try testing.allocator.alloc(u8, 4096);
1104
1446
  defer testing.allocator.free(output);
1105
1447
 
1106
- state_machine.prepare(0, .create_accounts, input);
1107
- const size = state_machine.commit(0, .create_accounts, input, output);
1448
+ _ = state_machine.prepare(.create_accounts, input);
1449
+ const size = state_machine.commit(0, 0, .create_accounts, input, output);
1108
1450
  const results = mem.bytesAsSlice(CreateAccountsResult, output[0..size]);
1109
1451
 
1110
1452
  try expectEqualSlices(
@@ -1127,7 +1469,6 @@ test "linked accounts" {
1127
1469
  try expectEqual(accounts[8], state_machine.get_account(accounts[8].id).?.*);
1128
1470
  try expectEqual(accounts[11], state_machine.get_account(accounts[11].id).?.*);
1129
1471
  try expectEqual(accounts[12], state_machine.get_account(accounts[12].id).?.*);
1130
- try expectEqual(@as(u32, 5), state_machine.accounts.count());
1131
1472
 
1132
1473
  // TODO How can we test that events were in fact rolled back in LIFO order?
1133
1474
  // All our rollback handlers appear to be commutative.
@@ -1176,16 +1517,24 @@ test "create/lookup/rollback transfers" {
1176
1517
  }),
1177
1518
  };
1178
1519
 
1179
- var state_machine = try StateMachine.init(testing.allocator, accounts.len, 1, 0);
1180
- defer state_machine.deinit();
1520
+ var context: TestContext = undefined;
1521
+ try context.init(testing.allocator, .{
1522
+ .lsm_forest_node_count = 1,
1523
+ .cache_size_accounts = accounts.len,
1524
+ .cache_size_transfers = 1,
1525
+ .cache_size_posted = 0,
1526
+ });
1527
+ defer context.deinit(testing.allocator);
1528
+
1529
+ const state_machine = &context.state_machine;
1181
1530
 
1182
1531
  const input = mem.asBytes(&accounts);
1183
1532
 
1184
1533
  const output = try testing.allocator.alloc(u8, 4096);
1185
1534
  defer testing.allocator.free(output);
1186
1535
 
1187
- state_machine.prepare(0, .create_accounts, input);
1188
- const size = state_machine.commit(0, .create_accounts, input, output);
1536
+ _ = state_machine.prepare(.create_accounts, input);
1537
+ const size = state_machine.commit(0, 0, .create_accounts, input, output);
1189
1538
 
1190
1539
  const errors = mem.bytesAsSlice(CreateAccountsResult, output[0..size]);
1191
1540
  try expect(errors.len == 0);
@@ -1245,6 +1594,21 @@ test "create/lookup/rollback transfers" {
1245
1594
  .timestamp = timestamp,
1246
1595
  }),
1247
1596
  },
1597
+ .{
1598
+ .result = .id_must_not_be_int_max,
1599
+ .object = mem.zeroInit(Transfer, .{
1600
+ .id = math.maxInt(u128),
1601
+ .debit_account_id = 0,
1602
+ .credit_account_id = 0,
1603
+ .pending_id = 1,
1604
+ .timeout = 0,
1605
+ .ledger = 0,
1606
+ .code = 0,
1607
+ .flags = .{ .pending = true },
1608
+ .amount = 0,
1609
+ .timestamp = timestamp,
1610
+ }),
1611
+ },
1248
1612
  .{
1249
1613
  .result = .debit_account_id_must_not_be_zero,
1250
1614
  .object = mem.zeroInit(Transfer, .{
@@ -1260,6 +1624,21 @@ test "create/lookup/rollback transfers" {
1260
1624
  .timestamp = timestamp,
1261
1625
  }),
1262
1626
  },
1627
+ .{
1628
+ .result = .debit_account_id_must_not_be_int_max,
1629
+ .object = mem.zeroInit(Transfer, .{
1630
+ .id = 1,
1631
+ .debit_account_id = math.maxInt(u128),
1632
+ .credit_account_id = 0,
1633
+ .pending_id = 1,
1634
+ .timeout = 0,
1635
+ .ledger = 0,
1636
+ .code = 0,
1637
+ .flags = .{ .pending = true },
1638
+ .amount = 0,
1639
+ .timestamp = timestamp,
1640
+ }),
1641
+ },
1263
1642
  .{
1264
1643
  .result = .credit_account_id_must_not_be_zero,
1265
1644
  .object = mem.zeroInit(Transfer, .{
@@ -1275,6 +1654,21 @@ test "create/lookup/rollback transfers" {
1275
1654
  .timestamp = timestamp,
1276
1655
  }),
1277
1656
  },
1657
+ .{
1658
+ .result = .credit_account_id_must_not_be_int_max,
1659
+ .object = mem.zeroInit(Transfer, .{
1660
+ .id = 1,
1661
+ .debit_account_id = 100,
1662
+ .credit_account_id = math.maxInt(u128),
1663
+ .pending_id = 1,
1664
+ .timeout = 0,
1665
+ .ledger = 0,
1666
+ .code = 0,
1667
+ .flags = .{ .pending = true },
1668
+ .amount = 0,
1669
+ .timestamp = timestamp,
1670
+ }),
1671
+ },
1278
1672
  .{
1279
1673
  .result = .accounts_must_be_different,
1280
1674
  .object = mem.zeroInit(Transfer, .{
@@ -1716,27 +2110,27 @@ test "create/lookup/rollback transfers" {
1716
2110
  }
1717
2111
 
1718
2112
  // Transfer 3:
1719
- try test_account_balances(&state_machine, 1, 100 + 123, 200 + 3, 0, 7);
1720
- try test_account_balances(&state_machine, 3, 0, 7, 110 + 123, 210 + 3);
2113
+ try test_account_balances(state_machine, 1, 100 + 123, 200 + 3, 0, 7);
2114
+ try test_account_balances(state_machine, 3, 0, 7, 110 + 123, 210 + 3);
1721
2115
  state_machine.create_transfer_rollback(state_machine.get_transfer(3).?);
1722
- try test_account_balances(&state_machine, 1, 100 + 123, 200, 0, 7);
1723
- try test_account_balances(&state_machine, 3, 0, 7, 110 + 123, 210);
2116
+ try test_account_balances(state_machine, 1, 100 + 123, 200, 0, 7);
2117
+ try test_account_balances(state_machine, 3, 0, 7, 110 + 123, 210);
1724
2118
  try expect(state_machine.get_transfer(3) == null);
1725
2119
 
1726
2120
  // Transfer 2:
1727
- try test_account_balances(&state_machine, 1, 100 + 123, 200, 0, 7);
1728
- try test_account_balances(&state_machine, 3, 0, 7, 110 + 123, 210);
2121
+ try test_account_balances(state_machine, 1, 100 + 123, 200, 0, 7);
2122
+ try test_account_balances(state_machine, 3, 0, 7, 110 + 123, 210);
1729
2123
  state_machine.create_transfer_rollback(state_machine.get_transfer(2).?);
1730
- try test_account_balances(&state_machine, 1, 100 + 123, 200, 0, 0);
1731
- try test_account_balances(&state_machine, 3, 0, 0, 110 + 123, 210);
2124
+ try test_account_balances(state_machine, 1, 100 + 123, 200, 0, 0);
2125
+ try test_account_balances(state_machine, 3, 0, 0, 110 + 123, 210);
1732
2126
  try expect(state_machine.get_transfer(2) == null);
1733
2127
 
1734
2128
  // Transfer 1:
1735
- try test_account_balances(&state_machine, 1, 100 + 123, 200, 0, 0);
1736
- try test_account_balances(&state_machine, 3, 0, 0, 110 + 123, 210);
2129
+ try test_account_balances(state_machine, 1, 100 + 123, 200, 0, 0);
2130
+ try test_account_balances(state_machine, 3, 0, 0, 110 + 123, 210);
1737
2131
  state_machine.create_transfer_rollback(state_machine.get_transfer(1).?);
1738
- try test_account_balances(&state_machine, 1, 100, 200, 0, 0);
1739
- try test_account_balances(&state_machine, 3, 0, 0, 110, 210);
2132
+ try test_account_balances(state_machine, 1, 100, 200, 0, 0);
2133
+ try test_account_balances(state_machine, 3, 0, 0, 110, 210);
1740
2134
  try expect(state_machine.get_transfer(1) == null);
1741
2135
 
1742
2136
  for (accounts) |account| {
@@ -1803,8 +2197,16 @@ test "create/lookup/rollback 2-phase transfers" {
1803
2197
  }),
1804
2198
  };
1805
2199
 
1806
- var state_machine = try StateMachine.init(testing.allocator, accounts.len, 100, 1);
1807
- defer state_machine.deinit();
2200
+ var context: TestContext = undefined;
2201
+ try context.init(testing.allocator, .{
2202
+ .lsm_forest_node_count = 1,
2203
+ .cache_size_accounts = accounts.len,
2204
+ .cache_size_transfers = 100,
2205
+ .cache_size_posted = 1,
2206
+ });
2207
+ defer context.deinit(testing.allocator);
2208
+
2209
+ const state_machine = &context.state_machine;
1808
2210
 
1809
2211
  // Create accounts:
1810
2212
  const accounts_input = mem.asBytes(&accounts);
@@ -1812,9 +2214,9 @@ test "create/lookup/rollback 2-phase transfers" {
1812
2214
  const accounts_output = try testing.allocator.alloc(u8, 4096);
1813
2215
  defer testing.allocator.free(accounts_output);
1814
2216
 
1815
- state_machine.prepare(0, .create_accounts, accounts_input);
2217
+ const accounts_timestamp = state_machine.prepare(.create_accounts, accounts_input);
1816
2218
  {
1817
- const size = state_machine.commit(0, .create_accounts, accounts_input, accounts_output);
2219
+ const size = state_machine.commit(0, 0, .create_accounts, accounts_input, accounts_output);
1818
2220
  const errors = mem.bytesAsSlice(CreateAccountsResult, accounts_output[0..size]);
1819
2221
  try expectEqual(@as(usize, 0), errors.len);
1820
2222
  }
@@ -1828,9 +2230,10 @@ test "create/lookup/rollback 2-phase transfers" {
1828
2230
  const transfers_output = try testing.allocator.alloc(u8, 4096);
1829
2231
  defer testing.allocator.free(transfers_output);
1830
2232
 
1831
- state_machine.prepare(0, .create_transfers, transfers_input);
2233
+ const transfers_timestamp = state_machine.prepare(.create_transfers, transfers_input);
2234
+ try testing.expect(transfers_timestamp > accounts_timestamp);
1832
2235
  {
1833
- const size = state_machine.commit(0, .create_transfers, transfers_input, transfers_output);
2236
+ const size = state_machine.commit(0, 1, .create_transfers, transfers_input, transfers_output);
1834
2237
  const errors = mem.bytesAsSlice(CreateTransfersResult, transfers_output[0..size]);
1835
2238
  try expectEqual(@as(usize, 0), errors.len);
1836
2239
  }
@@ -1839,8 +2242,8 @@ test "create/lookup/rollback 2-phase transfers" {
1839
2242
  }
1840
2243
 
1841
2244
  // Test balances before posting:
1842
- try test_account_balances(&state_machine, 1, 52, 15, 0, 0);
1843
- try test_account_balances(&state_machine, 2, 0, 0, 52, 15);
2245
+ try test_account_balances(state_machine, 1, 52, 15, 0, 0);
2246
+ try test_account_balances(state_machine, 2, 0, 0, 52, 15);
1844
2247
 
1845
2248
  // Post pending transfers:
1846
2249
  const Vector = struct { result: CreateTransferResult, object: Transfer };
@@ -1935,6 +2338,23 @@ test "create/lookup/rollback 2-phase transfers" {
1935
2338
  .timestamp = timestamp + 1,
1936
2339
  }),
1937
2340
  },
2341
+ .{
2342
+ .result = .pending_id_must_not_be_int_max,
2343
+ .object = mem.zeroInit(Transfer, .{
2344
+ .id = 101,
2345
+ .debit_account_id = 10,
2346
+ .credit_account_id = 20,
2347
+ .user_data = 30,
2348
+ .pending_id = math.maxInt(u128),
2349
+ .ledger = 60,
2350
+ .code = 70,
2351
+ .flags = .{
2352
+ .void_pending_transfer = true,
2353
+ },
2354
+ .amount = 80,
2355
+ .timestamp = timestamp + 1,
2356
+ }),
2357
+ },
1938
2358
  .{
1939
2359
  .result = .pending_id_must_be_different,
1940
2360
  .object = mem.zeroInit(Transfer, .{
@@ -2340,48 +2760,48 @@ test "create/lookup/rollback 2-phase transfers" {
2340
2760
  }
2341
2761
 
2342
2762
  // Balances after posting:
2343
- try test_account_balances(&state_machine, 1, 15, 35, 0, 0);
2344
- try test_account_balances(&state_machine, 2, 0, 0, 15, 35);
2763
+ try test_account_balances(state_machine, 1, 15, 35, 0, 0);
2764
+ try test_account_balances(state_machine, 2, 0, 0, 15, 35);
2345
2765
 
2346
2766
  // Rollback posting transfer (different amount):
2347
2767
  assert(vectors[0].result == .ok);
2348
- try test_transfer_rollback(&state_machine, &vectors[0].object);
2349
- try test_account_balances(&state_machine, 1, 30, 22, 0, 0);
2350
- try test_account_balances(&state_machine, 2, 0, 0, 30, 22);
2768
+ try test_transfer_rollback(state_machine, &vectors[0].object);
2769
+ try test_account_balances(state_machine, 1, 30, 22, 0, 0);
2770
+ try test_account_balances(state_machine, 2, 0, 0, 30, 22);
2351
2771
 
2352
2772
  // Rollback voiding transfer:
2353
- assert(vectors[22].result == .ok);
2354
- try test_transfer_rollback(&state_machine, &vectors[22].object);
2355
- try test_account_balances(&state_machine, 1, 45, 22, 0, 0);
2356
- try test_account_balances(&state_machine, 2, 0, 0, 45, 22);
2773
+ assert(vectors[23].result == .ok);
2774
+ try test_transfer_rollback(state_machine, &vectors[23].object);
2775
+ try test_account_balances(state_machine, 1, 45, 22, 0, 0);
2776
+ try test_account_balances(state_machine, 2, 0, 0, 45, 22);
2357
2777
 
2358
2778
  // Rollback posting transfer (zero amount):
2359
- assert(vectors[25].result == .ok);
2360
- try test_transfer_rollback(&state_machine, &vectors[25].object);
2361
- try test_account_balances(&state_machine, 1, 52, 15, 0, 0);
2362
- try test_account_balances(&state_machine, 2, 0, 0, 52, 15);
2779
+ assert(vectors[26].result == .ok);
2780
+ try test_transfer_rollback(state_machine, &vectors[26].object);
2781
+ try test_account_balances(state_machine, 1, 52, 15, 0, 0);
2782
+ try test_account_balances(state_machine, 2, 0, 0, 52, 15);
2363
2783
 
2364
2784
  // Rollback all pending transfers:
2365
- try test_transfer_rollback(&state_machine, &transfers[1]);
2366
- try test_account_balances(&state_machine, 1, 37, 15, 0, 0);
2367
- try test_account_balances(&state_machine, 2, 0, 0, 37, 15);
2785
+ try test_transfer_rollback(state_machine, &transfers[1]);
2786
+ try test_account_balances(state_machine, 1, 37, 15, 0, 0);
2787
+ try test_account_balances(state_machine, 2, 0, 0, 37, 15);
2368
2788
 
2369
- try test_transfer_rollback(&state_machine, &transfers[2]);
2370
- try test_account_balances(&state_machine, 1, 22, 15, 0, 0);
2371
- try test_account_balances(&state_machine, 2, 0, 0, 22, 15);
2789
+ try test_transfer_rollback(state_machine, &transfers[2]);
2790
+ try test_account_balances(state_machine, 1, 22, 15, 0, 0);
2791
+ try test_account_balances(state_machine, 2, 0, 0, 22, 15);
2372
2792
 
2373
- try test_transfer_rollback(&state_machine, &transfers[3]);
2374
- try test_account_balances(&state_machine, 1, 7, 15, 0, 0);
2375
- try test_account_balances(&state_machine, 2, 0, 0, 7, 15);
2793
+ try test_transfer_rollback(state_machine, &transfers[3]);
2794
+ try test_account_balances(state_machine, 1, 7, 15, 0, 0);
2795
+ try test_account_balances(state_machine, 2, 0, 0, 7, 15);
2376
2796
 
2377
- try test_transfer_rollback(&state_machine, &transfers[4]);
2378
- try test_account_balances(&state_machine, 1, 0, 15, 0, 0);
2379
- try test_account_balances(&state_machine, 2, 0, 0, 0, 15);
2797
+ try test_transfer_rollback(state_machine, &transfers[4]);
2798
+ try test_account_balances(state_machine, 1, 0, 15, 0, 0);
2799
+ try test_account_balances(state_machine, 2, 0, 0, 0, 15);
2380
2800
 
2381
2801
  // Rollback transfer:
2382
- try test_transfer_rollback(&state_machine, &transfers[0]);
2383
- try test_account_balances(&state_machine, 1, 0, 0, 0, 0);
2384
- try test_account_balances(&state_machine, 2, 0, 0, 0, 0);
2802
+ try test_transfer_rollback(state_machine, &transfers[0]);
2803
+ try test_account_balances(state_machine, 1, 0, 0, 0, 0);
2804
+ try test_account_balances(state_machine, 2, 0, 0, 0, 0);
2385
2805
  }
2386
2806
 
2387
2807
  fn print_test_vector(
@@ -2401,7 +2821,7 @@ fn print_test_vector(
2401
2821
  }
2402
2822
 
2403
2823
  fn test_account_balances(
2404
- state_machine: *const StateMachine,
2824
+ state_machine: *TestContext.StateMachine,
2405
2825
  account_id: u128,
2406
2826
  debits_pending: u64,
2407
2827
  debits_posted: u64,
@@ -2415,7 +2835,7 @@ fn test_account_balances(
2415
2835
  try expectEqual(credits_posted, account.credits_posted);
2416
2836
  }
2417
2837
 
2418
- fn test_transfer_rollback(state_machine: *StateMachine, transfer: *const Transfer) !void {
2838
+ fn test_transfer_rollback(state_machine: *TestContext.StateMachine, transfer: *const Transfer) !void {
2419
2839
  assert(state_machine.get_transfer(transfer.id) != null);
2420
2840
 
2421
2841
  state_machine.create_transfer_rollback(transfer);
@@ -2478,3 +2898,10 @@ fn test_equal_n_bytes(comptime n: usize) !void {
2478
2898
  }
2479
2899
  try expectEqual(true, routine(a, b));
2480
2900
  }
2901
+
2902
+ test "StateMachine: ref all decls" {
2903
+ const Storage = @import("storage.zig").Storage;
2904
+ const StateMachine = StateMachineType(Storage);
2905
+
2906
+ std.testing.refAllDecls(StateMachine);
2907
+ }