tigerbeetle-node 0.5.2 → 0.8.1

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 (61) hide show
  1. package/README.md +97 -78
  2. package/dist/benchmark.js +96 -94
  3. package/dist/benchmark.js.map +1 -1
  4. package/dist/index.d.ts +82 -82
  5. package/dist/index.js +74 -93
  6. package/dist/index.js.map +1 -1
  7. package/dist/test.js +134 -111
  8. package/dist/test.js.map +1 -1
  9. package/package.json +3 -2
  10. package/scripts/download_node_headers.sh +3 -1
  11. package/src/benchmark.ts +114 -118
  12. package/src/index.ts +102 -111
  13. package/src/node.zig +55 -63
  14. package/src/test.ts +146 -125
  15. package/src/tigerbeetle/scripts/benchmark.bat +46 -0
  16. package/src/tigerbeetle/scripts/benchmark.sh +5 -0
  17. package/src/tigerbeetle/scripts/install_zig.bat +109 -109
  18. package/src/tigerbeetle/scripts/install_zig.sh +8 -4
  19. package/src/tigerbeetle/scripts/vopr.bat +47 -47
  20. package/src/tigerbeetle/scripts/vopr.sh +2 -2
  21. package/src/tigerbeetle/src/benchmark.zig +65 -102
  22. package/src/tigerbeetle/src/cli.zig +39 -18
  23. package/src/tigerbeetle/src/config.zig +44 -25
  24. package/src/tigerbeetle/src/demo.zig +2 -15
  25. package/src/tigerbeetle/src/demo_01_create_accounts.zig +10 -10
  26. package/src/tigerbeetle/src/demo_03_create_transfers.zig +5 -3
  27. package/src/tigerbeetle/src/{demo_04_create_transfers_two_phase_commit.zig → demo_04_create_pending_transfers.zig} +18 -12
  28. package/src/tigerbeetle/src/demo_05_post_pending_transfers.zig +37 -0
  29. package/src/tigerbeetle/src/demo_06_void_pending_transfers.zig +24 -0
  30. package/src/tigerbeetle/src/demo_07_lookup_transfers.zig +1 -1
  31. package/src/tigerbeetle/src/io/benchmark.zig +24 -49
  32. package/src/tigerbeetle/src/io/darwin.zig +175 -44
  33. package/src/tigerbeetle/src/io/linux.zig +177 -72
  34. package/src/tigerbeetle/src/io/test.zig +61 -39
  35. package/src/tigerbeetle/src/io/windows.zig +1161 -0
  36. package/src/tigerbeetle/src/io.zig +2 -0
  37. package/src/tigerbeetle/src/main.zig +31 -10
  38. package/src/tigerbeetle/src/message_bus.zig +49 -61
  39. package/src/tigerbeetle/src/message_pool.zig +66 -57
  40. package/src/tigerbeetle/src/ring_buffer.zig +55 -3
  41. package/src/tigerbeetle/src/simulator.zig +108 -12
  42. package/src/tigerbeetle/src/state_machine.zig +1813 -816
  43. package/src/tigerbeetle/src/storage.zig +0 -230
  44. package/src/tigerbeetle/src/test/cluster.zig +168 -38
  45. package/src/tigerbeetle/src/test/message_bus.zig +4 -3
  46. package/src/tigerbeetle/src/test/network.zig +13 -16
  47. package/src/tigerbeetle/src/test/packet_simulator.zig +14 -1
  48. package/src/tigerbeetle/src/test/state_checker.zig +6 -3
  49. package/src/tigerbeetle/src/test/state_machine.zig +8 -7
  50. package/src/tigerbeetle/src/test/storage.zig +99 -40
  51. package/src/tigerbeetle/src/tigerbeetle.zig +108 -101
  52. package/src/tigerbeetle/src/time.zig +58 -11
  53. package/src/tigerbeetle/src/vsr/client.zig +18 -32
  54. package/src/tigerbeetle/src/vsr/clock.zig +1 -1
  55. package/src/tigerbeetle/src/vsr/journal.zig +1388 -464
  56. package/src/tigerbeetle/src/vsr/replica.zig +1340 -576
  57. package/src/tigerbeetle/src/vsr.zig +452 -40
  58. package/src/translate.zig +10 -0
  59. package/src/tigerbeetle/src/demo_05_accept_transfers.zig +0 -23
  60. package/src/tigerbeetle/src/demo_06_reject_transfers.zig +0 -17
  61. package/src/tigerbeetle/src/format_test.zig +0 -69
@@ -1,5 +1,7 @@
1
1
  const std = @import("std");
2
2
  const assert = std.debug.assert;
3
+ const math = std.math;
4
+ const mem = std.mem;
3
5
  const log = std.log.scoped(.state_machine);
4
6
 
5
7
  const tb = @import("tigerbeetle.zig");
@@ -10,49 +12,43 @@ const AccountFlags = tb.AccountFlags;
10
12
  const Transfer = tb.Transfer;
11
13
  const TransferFlags = tb.TransferFlags;
12
14
 
13
- const Commit = tb.Commit;
14
- const CommitFlags = tb.CommitFlags;
15
-
16
15
  const CreateAccountsResult = tb.CreateAccountsResult;
17
16
  const CreateTransfersResult = tb.CreateTransfersResult;
18
- const CommitTransfersResult = tb.CommitTransfersResult;
19
17
 
20
18
  const CreateAccountResult = tb.CreateAccountResult;
21
19
  const CreateTransferResult = tb.CreateTransferResult;
22
- const CommitTransferResult = tb.CommitTransferResult;
23
20
  const LookupAccountResult = tb.LookupAccountResult;
24
21
 
25
22
  const HashMapAccounts = std.AutoHashMap(u128, Account);
26
23
  const HashMapTransfers = std.AutoHashMap(u128, Transfer);
27
- const HashMapCommits = std.AutoHashMap(u128, Commit);
24
+ const HashMapPosted = std.AutoHashMap(u128, bool);
28
25
 
29
26
  pub const StateMachine = struct {
30
27
  pub const Operation = enum(u8) {
31
28
  /// Operations reserved by VR protocol (for all state machines):
32
29
  reserved,
33
- init,
30
+ root,
34
31
  register,
35
32
 
36
33
  /// Operations exported by TigerBeetle:
37
34
  create_accounts,
38
35
  create_transfers,
39
- commit_transfers,
40
36
  lookup_accounts,
41
37
  lookup_transfers,
42
38
  };
43
39
 
44
- allocator: std.mem.Allocator,
40
+ allocator: mem.Allocator,
45
41
  prepare_timestamp: u64,
46
42
  commit_timestamp: u64,
47
43
  accounts: HashMapAccounts,
48
44
  transfers: HashMapTransfers,
49
- commits: HashMapCommits,
45
+ posted: HashMapPosted,
50
46
 
51
47
  pub fn init(
52
- allocator: std.mem.Allocator,
48
+ allocator: mem.Allocator,
53
49
  accounts_max: usize,
54
50
  transfers_max: usize,
55
- commits_max: usize,
51
+ transfers_pending_max: usize,
56
52
  ) !StateMachine {
57
53
  var accounts = HashMapAccounts.init(allocator);
58
54
  errdefer accounts.deinit();
@@ -62,12 +58,9 @@ pub const StateMachine = struct {
62
58
  errdefer transfers.deinit();
63
59
  try transfers.ensureTotalCapacity(@intCast(u32, transfers_max));
64
60
 
65
- var commits = HashMapCommits.init(allocator);
66
- errdefer commits.deinit();
67
- try commits.ensureTotalCapacity(@intCast(u32, commits_max));
68
-
69
- // TODO After recovery, set prepare_timestamp max(wall clock, op timestamp).
70
- // TODO After recovery, set commit_timestamp max(wall clock, commit timestamp).
61
+ var posted = HashMapPosted.init(allocator);
62
+ errdefer posted.deinit();
63
+ try posted.ensureTotalCapacity(@intCast(u32, transfers_pending_max));
71
64
 
72
65
  return StateMachine{
73
66
  .allocator = allocator,
@@ -75,21 +68,20 @@ pub const StateMachine = struct {
75
68
  .commit_timestamp = 0,
76
69
  .accounts = accounts,
77
70
  .transfers = transfers,
78
- .commits = commits,
71
+ .posted = posted,
79
72
  };
80
73
  }
81
74
 
82
75
  pub fn deinit(self: *StateMachine) void {
83
76
  self.accounts.deinit();
84
77
  self.transfers.deinit();
85
- self.commits.deinit();
78
+ self.posted.deinit();
86
79
  }
87
80
 
88
81
  pub fn Event(comptime operation: Operation) type {
89
82
  return switch (operation) {
90
83
  .create_accounts => Account,
91
84
  .create_transfers => Transfer,
92
- .commit_transfers => Commit,
93
85
  .lookup_accounts => u128,
94
86
  .lookup_transfers => u128,
95
87
  else => unreachable,
@@ -100,42 +92,33 @@ pub const StateMachine = struct {
100
92
  return switch (operation) {
101
93
  .create_accounts => CreateAccountsResult,
102
94
  .create_transfers => CreateTransfersResult,
103
- .commit_transfers => CommitTransfersResult,
104
95
  .lookup_accounts => Account,
105
96
  .lookup_transfers => Transfer,
106
97
  else => unreachable,
107
98
  };
108
99
  }
109
100
 
110
- pub fn prepare(self: *StateMachine, realtime: i64, operation: Operation, input: []u8) void {
101
+ /// Returns the header's timestamp.
102
+ pub fn prepare(self: *StateMachine, operation: Operation, input: []u8) u64 {
111
103
  switch (operation) {
112
- .init => unreachable,
104
+ .root => unreachable,
113
105
  .register => {},
114
- .create_accounts => self.prepare_timestamps(realtime, .create_accounts, input),
115
- .create_transfers => self.prepare_timestamps(realtime, .create_transfers, input),
116
- .commit_transfers => self.prepare_timestamps(realtime, .commit_transfers, input),
106
+ .create_accounts => self.prepare_timestamps(.create_accounts, input),
107
+ .create_transfers => self.prepare_timestamps(.create_transfers, input),
117
108
  .lookup_accounts => {},
118
109
  .lookup_transfers => {},
119
110
  else => unreachable,
120
111
  }
112
+ return self.prepare_timestamp;
121
113
  }
122
114
 
123
115
  fn prepare_timestamps(
124
116
  self: *StateMachine,
125
- realtime: i64,
126
117
  comptime operation: Operation,
127
118
  input: []u8,
128
119
  ) void {
129
- // Guard against the wall clock going backwards by taking the max with timestamps issued:
130
- self.prepare_timestamp = std.math.max(
131
- // The cluster `commit_timestamp` may be ahead of our `prepare_timestamp` because this
132
- // may be our first prepare as a recently elected leader:
133
- std.math.max(self.prepare_timestamp, self.commit_timestamp) + 1,
134
- @intCast(u64, realtime),
135
- );
136
- assert(self.prepare_timestamp > self.commit_timestamp);
137
120
  var sum_reserved_timestamps: usize = 0;
138
- var events = std.mem.bytesAsSlice(Event(operation), input);
121
+ var events = mem.bytesAsSlice(Event(operation), input);
139
122
  for (events) |*event| {
140
123
  sum_reserved_timestamps += event.timestamp;
141
124
  self.prepare_timestamp += 1;
@@ -155,13 +138,11 @@ pub const StateMachine = struct {
155
138
  output: []u8,
156
139
  ) usize {
157
140
  _ = client;
158
-
159
141
  return switch (operation) {
160
- .init => unreachable,
142
+ .root => unreachable,
161
143
  .register => 0,
162
144
  .create_accounts => self.execute(.create_accounts, input, output),
163
145
  .create_transfers => self.execute(.create_transfers, input, output),
164
- .commit_transfers => self.execute(.commit_transfers, input, output),
165
146
  .lookup_accounts => self.execute_lookup_accounts(input, output),
166
147
  .lookup_transfers => self.execute_lookup_transfers(input, output),
167
148
  else => unreachable,
@@ -176,14 +157,14 @@ pub const StateMachine = struct {
176
157
  ) usize {
177
158
  comptime assert(operation != .lookup_accounts and operation != .lookup_transfers);
178
159
 
179
- const events = std.mem.bytesAsSlice(Event(operation), input);
180
- var results = std.mem.bytesAsSlice(Result(operation), output);
160
+ const events = mem.bytesAsSlice(Event(operation), input);
161
+ var results = mem.bytesAsSlice(Result(operation), output);
181
162
  var count: usize = 0;
182
163
 
183
164
  var chain: ?usize = null;
184
165
  var chain_broken = false;
185
166
 
186
- for (events) |event, index| {
167
+ for (events) |*event, index| {
187
168
  if (event.flags.linked and chain == null) {
188
169
  chain = index;
189
170
  assert(chain_broken == false);
@@ -191,7 +172,6 @@ pub const StateMachine = struct {
191
172
  const result = if (chain_broken) .linked_event_failed else switch (operation) {
192
173
  .create_accounts => self.create_account(event),
193
174
  .create_transfers => self.create_transfer(event),
194
- .commit_transfers => self.commit_transfer(event),
195
175
  else => unreachable,
196
176
  };
197
177
  log.debug("{s} {}/{}: {}: {}", .{
@@ -243,7 +223,7 @@ pub const StateMachine = struct {
243
223
  chain_start_index: usize,
244
224
  chain_error_index: usize,
245
225
  ) void {
246
- const events = std.mem.bytesAsSlice(Event(operation), input);
226
+ const events = mem.bytesAsSlice(Event(operation), input);
247
227
 
248
228
  // We commit events in FIFO order.
249
229
  // We must therefore rollback events in LIFO order with a reverse loop.
@@ -259,9 +239,8 @@ pub const StateMachine = struct {
259
239
  assert(event.timestamp <= self.commit_timestamp);
260
240
 
261
241
  switch (operation) {
262
- .create_accounts => self.create_account_rollback(event),
263
- .create_transfers => self.create_transfer_rollback(event),
264
- .commit_transfers => self.commit_transfer_rollback(event),
242
+ .create_accounts => self.create_account_rollback(&event),
243
+ .create_transfers => self.create_transfer_rollback(&event),
265
244
  else => unreachable,
266
245
  }
267
246
  log.debug("{s} {}/{}: rollback(): {}", .{
@@ -275,9 +254,9 @@ pub const StateMachine = struct {
275
254
  }
276
255
 
277
256
  fn execute_lookup_accounts(self: *StateMachine, input: []const u8, output: []u8) usize {
278
- const batch = std.mem.bytesAsSlice(u128, input);
257
+ const batch = mem.bytesAsSlice(u128, input);
279
258
  const output_len = @divFloor(output.len, @sizeOf(Account)) * @sizeOf(Account);
280
- const results = std.mem.bytesAsSlice(Account, output[0..output_len]);
259
+ const results = mem.bytesAsSlice(Account, output[0..output_len]);
281
260
  var results_count: usize = 0;
282
261
  for (batch) |id| {
283
262
  if (self.get_account(id)) |result| {
@@ -289,9 +268,9 @@ pub const StateMachine = struct {
289
268
  }
290
269
 
291
270
  fn execute_lookup_transfers(self: *StateMachine, input: []const u8, output: []u8) usize {
292
- const batch = std.mem.bytesAsSlice(u128, input);
271
+ const batch = mem.bytesAsSlice(u128, input);
293
272
  const output_len = @divFloor(output.len, @sizeOf(Transfer)) * @sizeOf(Transfer);
294
- const results = std.mem.bytesAsSlice(Transfer, output[0..output_len]);
273
+ const results = mem.bytesAsSlice(Transfer, output[0..output_len]);
295
274
  var results_count: usize = 0;
296
275
  for (batch) |id| {
297
276
  if (self.get_transfer(id)) |result| {
@@ -302,186 +281,337 @@ pub const StateMachine = struct {
302
281
  return results_count * @sizeOf(Transfer);
303
282
  }
304
283
 
305
- fn create_account(self: *StateMachine, a: Account) CreateAccountResult {
284
+ fn create_account(self: *StateMachine, a: *const Account) CreateAccountResult {
306
285
  assert(a.timestamp > self.commit_timestamp);
307
286
 
287
+ if (a.flags.padding != 0) return .reserved_flag;
308
288
  if (!zeroed_48_bytes(a.reserved)) return .reserved_field;
309
- if (a.flags.padding != 0) return .reserved_flag_padding;
289
+
290
+ if (a.id == 0) return .id_must_not_be_zero;
291
+ if (a.ledger == 0) return .ledger_must_not_be_zero;
292
+ if (a.code == 0) return .code_must_not_be_zero;
293
+
294
+ if (a.flags.debits_must_not_exceed_credits and a.flags.credits_must_not_exceed_debits) {
295
+ return .mutually_exclusive_flags;
296
+ }
297
+
298
+ if (sum_overflows(a.debits_pending, a.debits_posted)) return .overflows_debits;
299
+ if (sum_overflows(a.credits_pending, a.credits_posted)) return .overflows_credits;
310
300
 
311
301
  // Opening balances may never exceed limits:
312
302
  if (a.debits_exceed_credits(0)) return .exceeds_credits;
313
303
  if (a.credits_exceed_debits(0)) return .exceeds_debits;
314
304
 
315
- var insert = self.accounts.getOrPutAssumeCapacity(a.id);
316
- if (insert.found_existing) {
317
- const exists = insert.value_ptr.*;
318
- if (exists.unit != a.unit) return .exists_with_different_unit;
319
- if (exists.code != a.code) return .exists_with_different_code;
320
- if (@bitCast(u32, exists.flags) != @bitCast(u32, a.flags)) {
321
- return .exists_with_different_flags;
322
- }
323
- if (exists.user_data != a.user_data) return .exists_with_different_user_data;
324
- if (!equal_48_bytes(exists.reserved, a.reserved)) {
325
- return .exists_with_different_reserved_field;
326
- }
327
- return .exists;
328
- } else {
329
- insert.value_ptr.* = a;
330
- self.commit_timestamp = a.timestamp;
331
- return .ok;
332
- }
305
+ if (self.get_account(a.id)) |e| return create_account_exists(a, e);
306
+
307
+ self.accounts.putAssumeCapacityNoClobber(a.id, a.*);
308
+
309
+ self.commit_timestamp = a.timestamp;
310
+ return .ok;
333
311
  }
334
312
 
335
- fn create_account_rollback(self: *StateMachine, a: Account) void {
313
+ fn create_account_rollback(self: *StateMachine, a: *const Account) void {
336
314
  assert(self.accounts.remove(a.id));
337
315
  }
338
316
 
339
- fn create_transfer(self: *StateMachine, t: Transfer) CreateTransferResult {
317
+ fn create_account_exists(a: *const Account, e: *const Account) CreateAccountResult {
318
+ assert(a.id == e.id);
319
+ if (@bitCast(u16, a.flags) != @bitCast(u16, e.flags)) return .exists_with_different_flags;
320
+ if (a.user_data != e.user_data) return .exists_with_different_user_data;
321
+ assert(zeroed_48_bytes(a.reserved) and zeroed_48_bytes(e.reserved));
322
+ if (a.ledger != e.ledger) return .exists_with_different_ledger;
323
+ if (a.code != e.code) return .exists_with_different_code;
324
+ if (a.debits_pending != e.debits_pending) return .exists_with_different_debits_pending;
325
+ if (a.debits_posted != e.debits_posted) return .exists_with_different_debits_posted;
326
+ if (a.credits_pending != e.credits_pending) return .exists_with_different_credits_pending;
327
+ if (a.credits_posted != e.credits_posted) return .exists_with_different_credits_posted;
328
+ return .exists;
329
+ }
330
+
331
+ fn create_transfer(self: *StateMachine, t: *const Transfer) CreateTransferResult {
340
332
  assert(t.timestamp > self.commit_timestamp);
341
333
 
342
- if (t.flags.padding != 0) return .reserved_flag_padding;
343
- if (t.flags.two_phase_commit) {
344
- // Otherwise reserved amounts may never be released:
345
- if (t.timeout == 0) return .two_phase_commit_must_timeout;
346
- } else if (t.timeout != 0) {
347
- return .timeout_reserved_for_two_phase_commit;
334
+ if (t.flags.padding != 0) return .reserved_flag;
335
+ if (t.reserved != 0) return .reserved_field;
336
+
337
+ if (t.id == 0) return .id_must_not_be_zero;
338
+
339
+ if (t.flags.post_pending_transfer or t.flags.void_pending_transfer) {
340
+ return self.post_or_void_pending_transfer(t);
348
341
  }
349
- if (!t.flags.condition and !zeroed_32_bytes(t.reserved)) return .reserved_field;
350
342
 
351
- if (t.amount == 0) return .amount_is_zero;
343
+ if (t.debit_account_id == 0) return .debit_account_id_must_not_be_zero;
344
+ if (t.credit_account_id == 0) return .credit_account_id_must_not_be_zero;
345
+ if (t.credit_account_id == t.debit_account_id) return .accounts_must_be_different;
352
346
 
353
- if (t.debit_account_id == t.credit_account_id) return .accounts_are_the_same;
347
+ if (t.pending_id != 0) return .pending_id_must_be_zero;
348
+ if (t.flags.pending) {
349
+ // Otherwise, reserved amounts may never be released.
350
+ if (t.timeout == 0) return .pending_transfer_must_timeout;
351
+ } else {
352
+ if (t.timeout != 0) return .timeout_reserved_for_pending_transfer;
353
+ }
354
354
 
355
- // The etymology of the DR and CR abbreviations for debit and credit is interesting, either:
356
- // 1. derived from the Latin past participles of debitum and creditum, debere and credere,
355
+ if (t.ledger == 0) return .ledger_must_not_be_zero;
356
+ if (t.code == 0) return .code_must_not_be_zero;
357
+ if (t.amount == 0) return .amount_must_not_be_zero;
358
+
359
+ // The etymology of the DR and CR abbreviations for debit/credit is interesting, either:
360
+ // 1. derived from the Latin past participles of debitum/creditum, i.e. debere/credere,
357
361
  // 2. standing for debit record and credit record, or
358
362
  // 3. relating to debtor and creditor.
359
363
  // We use them to distinguish between `cr` (credit account), and `c` (commit).
360
- var dr = self.get_account(t.debit_account_id) orelse return .debit_account_not_found;
361
- var cr = self.get_account(t.credit_account_id) orelse return .credit_account_not_found;
364
+ const dr = self.get_account(t.debit_account_id) orelse return .debit_account_not_found;
365
+ const cr = self.get_account(t.credit_account_id) orelse return .credit_account_not_found;
366
+ assert(dr.id == t.debit_account_id);
367
+ assert(cr.id == t.credit_account_id);
362
368
  assert(t.timestamp > dr.timestamp);
363
369
  assert(t.timestamp > cr.timestamp);
364
370
 
365
- if (dr.unit != cr.unit) return .accounts_have_different_units;
371
+ if (dr.ledger != cr.ledger) return .accounts_must_have_the_same_ledger;
372
+ if (t.ledger != dr.ledger) return .transfer_must_have_the_same_ledger_as_accounts;
373
+
374
+ // If the transfer already exists, then it must not influence the overflow or limit checks.
375
+ if (self.get_transfer(t.id)) |e| return create_transfer_exists(t, e);
376
+
377
+ if (t.flags.pending) {
378
+ if (sum_overflows(t.amount, dr.debits_pending)) return .overflows_debits_pending;
379
+ if (sum_overflows(t.amount, cr.credits_pending)) return .overflows_credits_pending;
380
+ }
381
+ if (sum_overflows(t.amount, dr.debits_posted)) return .overflows_debits_posted;
382
+ if (sum_overflows(t.amount, cr.credits_posted)) return .overflows_credits_posted;
383
+ // We assert that the sum of the pending and posted balances can never overflow:
384
+ if (sum_overflows(t.amount, dr.debits_pending + dr.debits_posted)) {
385
+ return .overflows_debits;
386
+ }
387
+ if (sum_overflows(t.amount, cr.credits_pending + cr.credits_posted)) {
388
+ return .overflows_credits;
389
+ }
366
390
 
367
- // TODO We need a lookup before inserting in case transfer exists and would overflow limits.
368
- // If the transfer exists, then we should rather return .exists as an error.
369
391
  if (dr.debits_exceed_credits(t.amount)) return .exceeds_credits;
370
392
  if (cr.credits_exceed_debits(t.amount)) return .exceeds_debits;
371
393
 
372
- var insert = self.transfers.getOrPutAssumeCapacity(t.id);
373
- if (insert.found_existing) {
374
- const exists = insert.value_ptr.*;
375
- if (exists.debit_account_id != t.debit_account_id) {
376
- return .exists_with_different_debit_account_id;
377
- } else if (exists.credit_account_id != t.credit_account_id) {
378
- return .exists_with_different_credit_account_id;
379
- }
380
- if (exists.amount != t.amount) return .exists_with_different_amount;
381
- if (@bitCast(u32, exists.flags) != @bitCast(u32, t.flags)) {
382
- return .exists_with_different_flags;
383
- }
384
- if (exists.user_data != t.user_data) return .exists_with_different_user_data;
385
- if (!equal_32_bytes(exists.reserved, t.reserved)) {
386
- return .exists_with_different_reserved_field;
387
- }
388
- if (exists.timeout != t.timeout) return .exists_with_different_timeout;
389
- return .exists;
394
+ self.transfers.putAssumeCapacityNoClobber(t.id, t.*);
395
+
396
+ if (t.flags.pending) {
397
+ dr.debits_pending += t.amount;
398
+ cr.credits_pending += t.amount;
390
399
  } else {
391
- insert.value_ptr.* = t;
392
- if (t.flags.two_phase_commit) {
393
- dr.debits_reserved += t.amount;
394
- cr.credits_reserved += t.amount;
395
- } else {
396
- dr.debits_accepted += t.amount;
397
- cr.credits_accepted += t.amount;
398
- }
399
- self.commit_timestamp = t.timestamp;
400
- return .ok;
400
+ dr.debits_posted += t.amount;
401
+ cr.credits_posted += t.amount;
401
402
  }
403
+
404
+ self.commit_timestamp = t.timestamp;
405
+ return .ok;
402
406
  }
403
407
 
404
- fn create_transfer_rollback(self: *StateMachine, t: Transfer) void {
405
- var dr = self.get_account(t.debit_account_id).?;
406
- var cr = self.get_account(t.credit_account_id).?;
407
- if (t.flags.two_phase_commit) {
408
- dr.debits_reserved -= t.amount;
409
- cr.credits_reserved -= t.amount;
408
+ fn create_transfer_rollback(self: *StateMachine, t: *const Transfer) void {
409
+ if (t.flags.post_pending_transfer or t.flags.void_pending_transfer) {
410
+ return self.post_or_void_pending_transfer_rollback(t);
411
+ }
412
+
413
+ const dr = self.get_account(t.debit_account_id).?;
414
+ const cr = self.get_account(t.credit_account_id).?;
415
+ assert(dr.id == t.debit_account_id);
416
+ assert(cr.id == t.credit_account_id);
417
+
418
+ if (t.flags.pending) {
419
+ dr.debits_pending -= t.amount;
420
+ cr.credits_pending -= t.amount;
410
421
  } else {
411
- dr.debits_accepted -= t.amount;
412
- cr.credits_accepted -= t.amount;
422
+ dr.debits_posted -= t.amount;
423
+ cr.credits_posted -= t.amount;
413
424
  }
414
425
  assert(self.transfers.remove(t.id));
415
426
  }
416
427
 
417
- fn commit_transfer(self: *StateMachine, c: Commit) CommitTransferResult {
418
- assert(c.timestamp > self.commit_timestamp);
428
+ fn create_transfer_exists(t: *const Transfer, e: *const Transfer) CreateTransferResult {
429
+ assert(t.id == e.id);
430
+ // The flags change the behavior of the remaining comparisons, so compare the flags first.
431
+ if (@bitCast(u16, t.flags) != @bitCast(u16, e.flags)) return .exists_with_different_flags;
432
+ if (t.debit_account_id != e.debit_account_id) {
433
+ return .exists_with_different_debit_account_id;
434
+ }
435
+ if (t.credit_account_id != e.credit_account_id) {
436
+ return .exists_with_different_credit_account_id;
437
+ }
438
+ if (t.user_data != e.user_data) return .exists_with_different_user_data;
439
+ assert(t.reserved == 0 and e.reserved == 0);
440
+ assert(t.pending_id == 0 and e.pending_id == 0); // We know that the flags are the same.
441
+ if (t.timeout != e.timeout) return .exists_with_different_timeout;
442
+ assert(t.ledger == e.ledger); // If the accounts are the same, the ledger must be the same.
443
+ if (t.code != e.code) return .exists_with_different_code;
444
+ if (t.amount != e.amount) return .exists_with_different_amount;
445
+ return .exists;
446
+ }
419
447
 
420
- if (!c.flags.preimage and !zeroed_32_bytes(c.reserved)) return .reserved_field;
421
- if (c.flags.padding != 0) return .reserved_flag_padding;
448
+ fn post_or_void_pending_transfer(self: *StateMachine, t: *const Transfer) CreateTransferResult {
449
+ assert(t.id != 0);
450
+ assert(t.flags.padding == 0);
451
+ assert(t.reserved == 0);
452
+ assert(t.timestamp > self.commit_timestamp);
453
+ assert(t.flags.post_pending_transfer or t.flags.void_pending_transfer);
422
454
 
423
- var t = self.get_transfer(c.id) orelse return .transfer_not_found;
424
- assert(c.timestamp > t.timestamp);
455
+ if (t.flags.post_pending_transfer and t.flags.void_pending_transfer) {
456
+ return .cannot_post_and_void_pending_transfer;
457
+ }
458
+ if (t.flags.pending) return .pending_transfer_cannot_post_or_void_another;
459
+ if (t.timeout != 0) return .timeout_reserved_for_pending_transfer;
460
+
461
+ if (t.pending_id == 0) return .pending_id_must_not_be_zero;
462
+ if (t.pending_id == t.id) return .pending_id_must_be_different;
463
+
464
+ const p = self.get_transfer(t.pending_id) orelse return .pending_transfer_not_found;
465
+ assert(p.id == t.pending_id);
466
+ if (!p.flags.pending) return .pending_transfer_not_pending;
467
+
468
+ const dr = self.get_account(p.debit_account_id).?;
469
+ const cr = self.get_account(p.credit_account_id).?;
470
+ assert(dr.id == p.debit_account_id);
471
+ assert(cr.id == p.credit_account_id);
472
+ assert(p.timestamp > dr.timestamp);
473
+ assert(p.timestamp > cr.timestamp);
474
+ assert(p.amount > 0);
475
+
476
+ if (t.debit_account_id > 0 and t.debit_account_id != p.debit_account_id) {
477
+ return .pending_transfer_has_different_debit_account_id;
478
+ }
479
+ if (t.credit_account_id > 0 and t.credit_account_id != p.credit_account_id) {
480
+ return .pending_transfer_has_different_credit_account_id;
481
+ }
482
+ // The user_data field is allowed to differ across pending and posting/voiding transfers.
483
+ if (t.ledger > 0 and t.ledger != p.ledger) return .pending_transfer_has_different_ledger;
484
+ if (t.code > 0 and t.code != p.code) return .pending_transfer_has_different_code;
425
485
 
426
- if (!t.flags.two_phase_commit) return .transfer_not_two_phase_commit;
486
+ const amount = if (t.amount > 0) t.amount else p.amount;
487
+ if (amount > p.amount) return .exceeds_pending_transfer_amount;
427
488
 
428
- if (self.get_commit(c.id)) |exists| {
429
- if (!exists.flags.reject and c.flags.reject) return .already_committed_but_accepted;
430
- if (exists.flags.reject and !c.flags.reject) return .already_committed_but_rejected;
431
- return .already_committed;
489
+ if (t.flags.void_pending_transfer and amount < p.amount) {
490
+ return .pending_transfer_has_different_amount;
432
491
  }
433
492
 
434
- if (t.timeout > 0 and t.timestamp + t.timeout <= c.timestamp) return .transfer_expired;
493
+ if (self.get_transfer(t.id)) |e| return post_or_void_pending_transfer_exists(t, e, p);
435
494
 
436
- if (t.flags.condition) {
437
- if (!c.flags.preimage) return .condition_requires_preimage;
438
- if (!valid_preimage(t.reserved, c.reserved)) return .preimage_invalid;
439
- } else if (c.flags.preimage) {
440
- return .preimage_requires_condition;
495
+ if (self.get_posted(t.pending_id)) |posted| {
496
+ if (posted) return .pending_transfer_already_posted;
497
+ return .pending_transfer_already_voided;
441
498
  }
442
499
 
443
- var dr = self.get_account(t.debit_account_id) orelse return .debit_account_not_found;
444
- var cr = self.get_account(t.credit_account_id) orelse return .credit_account_not_found;
445
- assert(t.timestamp > dr.timestamp);
446
- assert(t.timestamp > cr.timestamp);
500
+ assert(p.timestamp < t.timestamp);
501
+ assert(p.timeout > 0);
502
+ if (p.timestamp + p.timeout <= t.timestamp) return .pending_transfer_expired;
503
+
504
+ self.transfers.putAssumeCapacityNoClobber(t.id, .{
505
+ .id = t.id,
506
+ .debit_account_id = p.debit_account_id,
507
+ .credit_account_id = p.credit_account_id,
508
+ .user_data = if (t.user_data > 0) t.user_data else p.user_data,
509
+ .reserved = p.reserved,
510
+ .ledger = p.ledger,
511
+ .code = p.code,
512
+ .pending_id = t.pending_id,
513
+ .timeout = t.timeout,
514
+ .timestamp = t.timestamp,
515
+ .flags = t.flags,
516
+ .amount = amount,
517
+ });
518
+
519
+ self.posted.putAssumeCapacityNoClobber(t.pending_id, t.flags.post_pending_transfer);
520
+
521
+ dr.debits_pending -= p.amount;
522
+ cr.credits_pending -= p.amount;
523
+
524
+ if (t.flags.post_pending_transfer) {
525
+ assert(amount > 0);
526
+ assert(amount <= p.amount);
527
+ dr.debits_posted += amount;
528
+ cr.credits_posted += amount;
529
+ }
447
530
 
448
- assert(t.flags.two_phase_commit);
449
- if (dr.debits_reserved < t.amount) return .debit_amount_was_not_reserved;
450
- if (cr.credits_reserved < t.amount) return .credit_amount_was_not_reserved;
531
+ self.commit_timestamp = t.timestamp;
532
+ return .ok;
533
+ }
534
+
535
+ fn post_or_void_pending_transfer_rollback(self: *StateMachine, t: *const Transfer) void {
536
+ assert(t.id > 0);
537
+ assert(t.flags.post_pending_transfer or t.flags.void_pending_transfer);
538
+
539
+ assert(t.pending_id > 0);
540
+ const p = self.get_transfer(t.pending_id).?;
541
+ assert(p.id == t.pending_id);
542
+ assert(p.debit_account_id > 0);
543
+ assert(p.credit_account_id > 0);
544
+
545
+ const dr = self.get_account(p.debit_account_id).?;
546
+ const cr = self.get_account(p.credit_account_id).?;
547
+ assert(dr.id == p.debit_account_id);
548
+ assert(cr.id == p.credit_account_id);
549
+
550
+ if (t.flags.post_pending_transfer) {
551
+ const amount = if (t.amount > 0) t.amount else p.amount;
552
+ assert(amount > 0);
553
+ assert(amount <= p.amount);
554
+ dr.debits_posted -= amount;
555
+ cr.credits_posted -= amount;
556
+ }
557
+ dr.debits_pending += p.amount;
558
+ cr.credits_pending += p.amount;
451
559
 
452
- // Once reserved, the amount can be moved from reserved to accepted without breaking limits:
453
- assert(!dr.debits_exceed_credits(0));
454
- assert(!cr.credits_exceed_debits(0));
560
+ assert(self.posted.remove(t.pending_id));
561
+ assert(self.transfers.remove(t.id));
562
+ }
563
+
564
+ fn post_or_void_pending_transfer_exists(
565
+ t: *const Transfer,
566
+ e: *const Transfer,
567
+ p: *const Transfer,
568
+ ) CreateTransferResult {
569
+ assert(p.flags.pending);
570
+ assert(t.pending_id == p.id);
571
+ assert(t.id != p.id);
572
+ assert(t.id == e.id);
573
+
574
+ // Do not assume that `e` is necessarily a posting or voiding transfer.
575
+ if (@bitCast(u16, t.flags) != @bitCast(u16, e.flags)) {
576
+ return .exists_with_different_flags;
577
+ }
455
578
 
456
- // TODO We can combine this lookup with the previous lookup if we return `error!void`:
457
- var insert = self.commits.getOrPutAssumeCapacity(c.id);
458
- if (insert.found_existing) {
459
- unreachable;
579
+ // If `e` posted or voided a different pending transfer, then the accounts will differ.
580
+ if (t.pending_id != e.pending_id) return .exists_with_different_pending_id;
581
+
582
+ assert(e.flags.post_pending_transfer or e.flags.void_pending_transfer);
583
+ assert(e.debit_account_id == p.debit_account_id);
584
+ assert(e.credit_account_id == p.credit_account_id);
585
+ assert(e.reserved == 0);
586
+ assert(e.pending_id == p.id);
587
+ assert(e.timeout == 0);
588
+ assert(e.ledger == p.ledger);
589
+ assert(e.code == p.code);
590
+ assert(e.timestamp > p.timestamp);
591
+
592
+ assert(t.flags.post_pending_transfer == e.flags.post_pending_transfer);
593
+ assert(t.flags.void_pending_transfer == e.flags.void_pending_transfer);
594
+ assert(t.debit_account_id == 0 or t.debit_account_id == e.debit_account_id);
595
+ assert(t.credit_account_id == 0 or t.credit_account_id == e.credit_account_id);
596
+ assert(t.reserved == 0);
597
+ assert(t.timeout == 0);
598
+ assert(t.ledger == 0 or t.ledger == e.ledger);
599
+ assert(t.code == 0 or t.code == e.code);
600
+ assert(t.timestamp > e.timestamp);
601
+
602
+ if (t.user_data == 0) {
603
+ if (e.user_data != p.user_data) return .exists_with_different_user_data;
460
604
  } else {
461
- insert.value_ptr.* = c;
462
- dr.debits_reserved -= t.amount;
463
- cr.credits_reserved -= t.amount;
464
- if (!c.flags.reject) {
465
- dr.debits_accepted += t.amount;
466
- cr.credits_accepted += t.amount;
467
- }
468
- self.commit_timestamp = c.timestamp;
469
- return .ok;
605
+ if (t.user_data != e.user_data) return .exists_with_different_user_data;
470
606
  }
471
- }
472
607
 
473
- fn commit_transfer_rollback(self: *StateMachine, c: Commit) void {
474
- assert(self.get_commit(c.id) != null);
475
- var t = self.get_transfer(c.id).?;
476
- var dr = self.get_account(t.debit_account_id).?;
477
- var cr = self.get_account(t.credit_account_id).?;
478
- dr.debits_reserved += t.amount;
479
- cr.credits_reserved += t.amount;
480
- if (!c.flags.reject) {
481
- dr.debits_accepted -= t.amount;
482
- cr.credits_accepted -= t.amount;
608
+ if (t.amount == 0) {
609
+ if (e.amount != p.amount) return .exists_with_different_amount;
610
+ } else {
611
+ if (t.amount != e.amount) return .exists_with_different_amount;
483
612
  }
484
- assert(self.commits.remove(c.id));
613
+
614
+ return .exists;
485
615
  }
486
616
 
487
617
  /// This is our core private method for changing balances.
@@ -490,30 +620,24 @@ pub const StateMachine = struct {
490
620
  /// This pointer is invalidated if the hash map is resized by another insert, e.g. if we get a
491
621
  /// pointer, insert another account without capacity, and then modify this pointer... BOOM!
492
622
  /// This is a sharp tool but replaces a lookup, copy and update with a single lookup.
493
- fn get_account(self: *StateMachine, id: u128) ?*Account {
623
+ fn get_account(self: *const StateMachine, id: u128) ?*Account {
494
624
  return self.accounts.getPtr(id);
495
625
  }
496
626
 
497
627
  /// See the comment for get_account().
498
- fn get_transfer(self: *StateMachine, id: u128) ?*Transfer {
628
+ fn get_transfer(self: *const StateMachine, id: u128) ?*Transfer {
499
629
  return self.transfers.getPtr(id);
500
630
  }
501
631
 
502
- /// See the comment for get_account().
503
- fn get_commit(self: *StateMachine, id: u128) ?*Commit {
504
- return self.commits.getPtr(id);
632
+ /// Returns whether a pending transfer, if it exists, has already been posted or voided.
633
+ fn get_posted(self: *const StateMachine, pending_id: u128) ?bool {
634
+ return self.posted.get(pending_id);
505
635
  }
506
636
  };
507
637
 
508
- // TODO Optimize this by precomputing hashes outside and before committing to the state machine.
509
- // If we see that a batch of commits contains commits with preimages, then we will:
510
- // Divide the batch into subsets, dispatch these to multiple threads, store the result in a bitset.
511
- // Then we can simply provide the result bitset to the state machine when committing.
512
- // This will improve crypto performance significantly by a factor of 8x.
513
- fn valid_preimage(condition: [32]u8, preimage: [32]u8) bool {
514
- var target: [32]u8 = undefined;
515
- std.crypto.hash.sha2.Sha256.hash(&preimage, &target, .{});
516
- return std.crypto.utils.timingSafeEql([32]u8, target, condition);
638
+ fn sum_overflows(a: u64, b: u64) bool {
639
+ var c: u64 = undefined;
640
+ return @addWithOverflow(u64, a, b, &c);
517
641
  }
518
642
 
519
643
  /// Optimizes for the common case, where the array is zeroed. Completely branchless.
@@ -550,924 +674,1797 @@ fn equal_48_bytes(a: [48]u8, b: [48]u8) bool {
550
674
  }
551
675
 
552
676
  const testing = std.testing;
677
+ const expect = testing.expect;
678
+ const expectEqual = testing.expectEqual;
679
+ const expectEqualSlices = testing.expectEqualSlices;
680
+
681
+ test "sum_overflows" {
682
+ try expectEqual(false, sum_overflows(math.maxInt(u64), 0));
683
+ try expectEqual(false, sum_overflows(math.maxInt(u64) - 1, 1));
684
+ try expectEqual(false, sum_overflows(1, math.maxInt(u64) - 1));
553
685
 
554
- test "create/lookup accounts" {
555
- var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
556
- defer arena.deinit();
686
+ try expectEqual(true, sum_overflows(math.maxInt(u64), 1));
687
+ try expectEqual(true, sum_overflows(1, math.maxInt(u64)));
557
688
 
558
- const allocator = arena.allocator();
689
+ try expectEqual(true, sum_overflows(math.maxInt(u64), math.maxInt(u64)));
690
+ try expectEqual(true, sum_overflows(math.maxInt(u64), math.maxInt(u64)));
691
+ }
559
692
 
693
+ test "create/lookup/rollback accounts" {
560
694
  const Vector = struct { result: CreateAccountResult, object: Account };
561
695
 
562
696
  const vectors = [_]Vector{
563
- Vector{
564
- .result = .reserved_flag_padding,
565
- .object = std.mem.zeroInit(Account, .{
697
+ .{
698
+ .result = .ok,
699
+ .object = mem.zeroInit(Account, .{
566
700
  .id = 1,
701
+ .user_data = 2,
702
+ .ledger = 3,
703
+ .code = 4,
704
+ .debits_pending = 5,
705
+ .debits_posted = 6,
706
+ .credits_pending = 7,
707
+ .credits_posted = 8,
567
708
  .timestamp = 1,
568
- .flags = .{ .padding = 1 },
569
709
  }),
570
710
  },
571
- Vector{
711
+ .{
712
+ .result = .reserved_flag,
713
+ .object = mem.zeroInit(Account, .{
714
+ .id = 0,
715
+ .user_data = 0,
716
+ .reserved = [_]u8{1} ** 48,
717
+ .ledger = 0,
718
+ .code = 0,
719
+ .flags = .{
720
+ .padding = 1,
721
+ .debits_must_not_exceed_credits = true,
722
+ .credits_must_not_exceed_debits = true,
723
+ },
724
+ .debits_pending = math.maxInt(u64),
725
+ .debits_posted = math.maxInt(u64),
726
+ .credits_pending = math.maxInt(u64),
727
+ .credits_posted = math.maxInt(u64),
728
+ .timestamp = 2,
729
+ }),
730
+ },
731
+ .{
572
732
  .result = .reserved_field,
573
- .object = std.mem.zeroInit(Account, .{
574
- .id = 2,
575
- .timestamp = 1,
733
+ .object = mem.zeroInit(Account, .{
734
+ .id = 0,
735
+ .user_data = 0,
576
736
  .reserved = [_]u8{1} ** 48,
737
+ .ledger = 0,
738
+ .code = 0,
739
+ .flags = .{
740
+ .padding = 0,
741
+ .debits_must_not_exceed_credits = true,
742
+ .credits_must_not_exceed_debits = true,
743
+ },
744
+ .debits_pending = math.maxInt(u64),
745
+ .debits_posted = math.maxInt(u64),
746
+ .credits_pending = math.maxInt(u64),
747
+ .credits_posted = math.maxInt(u64),
748
+ .timestamp = 2,
577
749
  }),
578
750
  },
579
- Vector{
580
- .result = .exceeds_credits,
581
- .object = std.mem.zeroInit(Account, .{
582
- .id = 3,
583
- .timestamp = 1,
584
- .debits_reserved = 10,
585
- .flags = .{ .debits_must_not_exceed_credits = true },
751
+ .{
752
+ .result = .id_must_not_be_zero,
753
+ .object = mem.zeroInit(Account, .{
754
+ .id = 0,
755
+ .user_data = 0,
756
+ .ledger = 0,
757
+ .code = 0,
758
+ .flags = .{
759
+ .padding = 0,
760
+ .debits_must_not_exceed_credits = true,
761
+ .credits_must_not_exceed_debits = true,
762
+ },
763
+ .debits_pending = math.maxInt(u64),
764
+ .debits_posted = math.maxInt(u64),
765
+ .credits_pending = math.maxInt(u64),
766
+ .credits_posted = math.maxInt(u64),
767
+ .timestamp = 2,
586
768
  }),
587
769
  },
588
- Vector{
589
- .result = .exceeds_credits,
590
- .object = std.mem.zeroInit(Account, .{
591
- .id = 4,
592
- .timestamp = 1,
593
- .debits_accepted = 10,
594
- .flags = .{ .debits_must_not_exceed_credits = true },
770
+ .{
771
+ .result = .ledger_must_not_be_zero,
772
+ .object = mem.zeroInit(Account, .{
773
+ .id = 1,
774
+ .user_data = 20,
775
+ .ledger = 0,
776
+ .code = 0,
777
+ .flags = .{
778
+ .padding = 0,
779
+ .debits_must_not_exceed_credits = true,
780
+ .credits_must_not_exceed_debits = true,
781
+ },
782
+ .debits_pending = math.maxInt(u64),
783
+ .debits_posted = math.maxInt(u64),
784
+ .credits_pending = math.maxInt(u64),
785
+ .credits_posted = math.maxInt(u64),
786
+ .timestamp = 2,
595
787
  }),
596
788
  },
597
- Vector{
598
- .result = .exceeds_debits,
599
- .object = std.mem.zeroInit(Account, .{
600
- .id = 5,
601
- .timestamp = 1,
602
- .credits_reserved = 10,
603
- .flags = .{ .credits_must_not_exceed_debits = true },
789
+ .{
790
+ .result = .code_must_not_be_zero,
791
+ .object = mem.zeroInit(Account, .{
792
+ .id = 1,
793
+ .user_data = 20,
794
+ .ledger = 30,
795
+ .code = 0,
796
+ .flags = .{
797
+ .debits_must_not_exceed_credits = true,
798
+ .credits_must_not_exceed_debits = true,
799
+ },
800
+ .debits_pending = math.maxInt(u64),
801
+ .debits_posted = math.maxInt(u64),
802
+ .credits_pending = math.maxInt(u64),
803
+ .credits_posted = math.maxInt(u64),
804
+ .timestamp = 2,
604
805
  }),
605
806
  },
606
- Vector{
607
- .result = .exceeds_debits,
608
- .object = std.mem.zeroInit(Account, .{
609
- .id = 6,
610
- .timestamp = 1,
611
- .credits_accepted = 10,
612
- .flags = .{ .credits_must_not_exceed_debits = true },
807
+ .{
808
+ .result = .mutually_exclusive_flags,
809
+ .object = mem.zeroInit(Account, .{
810
+ .id = 1,
811
+ .user_data = 20,
812
+ .ledger = 30,
813
+ .code = 40,
814
+ .flags = .{
815
+ .debits_must_not_exceed_credits = true,
816
+ .credits_must_not_exceed_debits = true,
817
+ },
818
+ .debits_pending = math.maxInt(u64),
819
+ .debits_posted = math.maxInt(u64),
820
+ .credits_pending = math.maxInt(u64),
821
+ .credits_posted = math.maxInt(u64),
822
+ .timestamp = 2,
613
823
  }),
614
824
  },
615
- Vector{
616
- .result = .ok,
617
- .object = std.mem.zeroInit(Account, .{
618
- .id = 7,
619
- .timestamp = 1,
825
+ .{
826
+ .result = .overflows_debits,
827
+ .object = mem.zeroInit(Account, .{
828
+ .id = 1,
829
+ .user_data = 20,
830
+ .ledger = 30,
831
+ .code = 40,
832
+ .flags = .{
833
+ .debits_must_not_exceed_credits = true,
834
+ },
835
+ .debits_pending = math.maxInt(u64),
836
+ .debits_posted = 60,
837
+ .credits_pending = math.maxInt(u64),
838
+ .credits_posted = 80,
839
+ .timestamp = 2,
620
840
  }),
621
841
  },
622
- Vector{
623
- .result = .exists,
624
- .object = std.mem.zeroInit(Account, .{
625
- .id = 7,
842
+ .{
843
+ .result = .overflows_credits,
844
+ .object = mem.zeroInit(Account, .{
845
+ .id = 1,
846
+ .user_data = 20,
847
+ .ledger = 30,
848
+ .code = 40,
849
+ .flags = .{
850
+ .credits_must_not_exceed_debits = true,
851
+ },
852
+ .debits_pending = 50,
853
+ .debits_posted = 60,
854
+ .credits_pending = math.maxInt(u64),
855
+ .credits_posted = 80,
626
856
  .timestamp = 2,
627
857
  }),
628
858
  },
629
- Vector{
630
- .result = .ok,
631
- .object = std.mem.zeroInit(Account, .{
632
- .id = 8,
859
+ .{
860
+ .result = .exceeds_credits,
861
+ .object = mem.zeroInit(Account, .{
862
+ .id = 1,
863
+ .user_data = 20,
864
+ .ledger = 30,
865
+ .code = 40,
866
+ .flags = .{
867
+ .debits_must_not_exceed_credits = true,
868
+ },
869
+ .debits_pending = 50,
870
+ .debits_posted = 60,
871
+ .credits_pending = 1,
872
+ .credits_posted = 109,
633
873
  .timestamp = 2,
634
- .user_data = 'U',
635
- .unit = 9,
636
874
  }),
637
875
  },
638
- Vector{
639
- .result = .exists_with_different_unit,
640
- .object = std.mem.zeroInit(Account, .{
641
- .id = 8,
642
- .timestamp = 3,
643
- .user_data = 'U',
644
- .unit = 10,
876
+ .{
877
+ .result = .exceeds_debits,
878
+ .object = mem.zeroInit(Account, .{
879
+ .id = 1,
880
+ .user_data = 20,
881
+ .ledger = 30,
882
+ .code = 40,
883
+ .flags = .{
884
+ .credits_must_not_exceed_debits = true,
885
+ },
886
+ .debits_pending = 50,
887
+ .debits_posted = 60,
888
+ .credits_pending = 1,
889
+ .credits_posted = 109,
890
+ .timestamp = 2,
645
891
  }),
646
892
  },
647
- Vector{
648
- .result = .ok,
649
- .object = std.mem.zeroInit(Account, .{
650
- .id = 9,
651
- .timestamp = 3,
652
- .code = 9,
653
- .user_data = 'U',
893
+ .{
894
+ .result = .exists_with_different_flags,
895
+ .object = mem.zeroInit(Account, .{
896
+ .id = 1,
897
+ .user_data = 20,
898
+ .ledger = 30,
899
+ .code = 40,
900
+ .flags = .{
901
+ .credits_must_not_exceed_debits = true,
902
+ },
903
+ .debits_pending = 50,
904
+ .debits_posted = 60,
905
+ .credits_pending = 0,
906
+ .credits_posted = 0,
907
+ .timestamp = 2,
654
908
  }),
655
909
  },
656
- Vector{
910
+ .{
911
+ .result = .exists_with_different_user_data,
912
+ .object = mem.zeroInit(Account, .{
913
+ .id = 1,
914
+ .user_data = 20,
915
+ .ledger = 30,
916
+ .code = 40,
917
+ .debits_pending = 50,
918
+ .debits_posted = 60,
919
+ .credits_pending = 70,
920
+ .credits_posted = 80,
921
+ .timestamp = 2,
922
+ }),
923
+ },
924
+ .{
925
+ .result = .exists_with_different_ledger,
926
+ .object = mem.zeroInit(Account, .{
927
+ .id = 1,
928
+ .user_data = 2,
929
+ .ledger = 30,
930
+ .code = 40,
931
+ .debits_pending = 50,
932
+ .debits_posted = 60,
933
+ .credits_pending = 70,
934
+ .credits_posted = 80,
935
+ .timestamp = 2,
936
+ }),
937
+ },
938
+ .{
657
939
  .result = .exists_with_different_code,
658
- .object = std.mem.zeroInit(Account, .{
659
- .id = 9,
660
- .timestamp = 4,
661
- .code = 10,
662
- .user_data = 'D',
940
+ .object = mem.zeroInit(Account, .{
941
+ .id = 1,
942
+ .user_data = 2,
943
+ .ledger = 3,
944
+ .code = 40,
945
+ .debits_pending = 50,
946
+ .debits_posted = 60,
947
+ .credits_pending = 70,
948
+ .credits_posted = 80,
949
+ .timestamp = 2,
663
950
  }),
664
951
  },
665
- Vector{
666
- .result = .ok,
667
- .object = std.mem.zeroInit(Account, .{
668
- .id = 10,
669
- .timestamp = 4,
670
- .flags = .{ .credits_must_not_exceed_debits = true },
952
+ .{
953
+ .result = .exists_with_different_debits_pending,
954
+ .object = mem.zeroInit(Account, .{
955
+ .id = 1,
956
+ .user_data = 2,
957
+ .ledger = 3,
958
+ .code = 4,
959
+ .debits_pending = 50,
960
+ .debits_posted = 60,
961
+ .credits_pending = 70,
962
+ .credits_posted = 80,
963
+ .timestamp = 2,
671
964
  }),
672
965
  },
673
- Vector{
674
- .result = .exists_with_different_flags,
675
- .object = std.mem.zeroInit(Account, .{
676
- .id = 10,
677
- .timestamp = 5,
678
- .flags = .{ .debits_must_not_exceed_credits = true },
966
+ .{
967
+ .result = .exists_with_different_debits_posted,
968
+ .object = mem.zeroInit(Account, .{
969
+ .id = 1,
970
+ .user_data = 2,
971
+ .ledger = 3,
972
+ .code = 4,
973
+ .debits_pending = 5,
974
+ .debits_posted = 60,
975
+ .credits_pending = 70,
976
+ .credits_posted = 80,
977
+ .timestamp = 2,
679
978
  }),
680
979
  },
681
- Vector{
682
- .result = .ok,
683
- .object = std.mem.zeroInit(Account, .{
684
- .id = 11,
685
- .timestamp = 5,
686
- .user_data = 'U',
980
+ .{
981
+ .result = .exists_with_different_credits_pending,
982
+ .object = mem.zeroInit(Account, .{
983
+ .id = 1,
984
+ .user_data = 2,
985
+ .ledger = 3,
986
+ .code = 4,
987
+ .debits_pending = 5,
988
+ .debits_posted = 6,
989
+ .credits_pending = 70,
990
+ .credits_posted = 80,
991
+ .timestamp = 2,
687
992
  }),
688
993
  },
689
- Vector{
690
- .result = .exists_with_different_user_data,
691
- .object = std.mem.zeroInit(Account, .{
692
- .id = 11,
693
- .timestamp = 6,
694
- .user_data = 'D',
994
+ .{
995
+ .result = .exists_with_different_credits_posted,
996
+ .object = mem.zeroInit(Account, .{
997
+ .id = 1,
998
+ .user_data = 2,
999
+ .ledger = 3,
1000
+ .code = 4,
1001
+ .debits_pending = 5,
1002
+ .debits_posted = 6,
1003
+ .credits_pending = 7,
1004
+ .credits_posted = 80,
1005
+ .timestamp = 2,
1006
+ }),
1007
+ },
1008
+ .{
1009
+ .result = .exists,
1010
+ .object = mem.zeroInit(Account, .{
1011
+ .id = 1,
1012
+ .user_data = 2,
1013
+ .ledger = 3,
1014
+ .code = 4,
1015
+ .debits_pending = 5,
1016
+ .debits_posted = 6,
1017
+ .credits_pending = 7,
1018
+ .credits_posted = 8,
1019
+ .timestamp = 2,
695
1020
  }),
696
1021
  },
697
1022
  };
698
1023
 
699
- var state_machine = try StateMachine.init(allocator, vectors.len, 0, 0);
1024
+ var state_machine = try StateMachine.init(testing.allocator, vectors.len, 0, 0);
700
1025
  defer state_machine.deinit();
701
1026
 
702
- for (vectors) |vector| {
703
- try testing.expectEqual(vector.result, state_machine.create_account(vector.object));
1027
+ for (vectors) |vector, i| {
1028
+ const result = state_machine.create_account(&vector.object);
1029
+ expectEqual(vector.result, result) catch |err| {
1030
+ print_test_vector(i, vector.result, result, vector.object, err);
1031
+ return err;
1032
+ };
1033
+
704
1034
  if (vector.result == .ok) {
705
- try testing.expectEqual(vector.object, state_machine.get_account(vector.object.id).?.*);
1035
+ try expectEqual(vector.object, state_machine.get_account(vector.object.id).?.*);
706
1036
  }
707
1037
  }
1038
+
1039
+ state_machine.create_account_rollback(&vectors[0].object);
1040
+ try expect(state_machine.get_account(vectors[0].object.id) == null);
708
1041
  }
709
1042
 
710
1043
  test "linked accounts" {
711
- var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
712
- defer arena.deinit();
713
-
714
- const allocator = arena.allocator();
715
-
716
1044
  const accounts_max = 5;
717
1045
  const transfers_max = 0;
718
- const commits_max = 0;
1046
+ const transfers_pending_max = 0;
719
1047
 
720
1048
  var accounts = [_]Account{
721
1049
  // An individual event (successful):
722
- std.mem.zeroInit(Account, .{ .id = 7, .code = 200 }),
1050
+ mem.zeroInit(Account, .{ .id = 7, .code = 1, .ledger = 1 }),
723
1051
 
724
1052
  // A chain of 4 events (the last event in the chain closes the chain with linked=false):
725
- // Commit/rollback:
726
- std.mem.zeroInit(Account, .{ .id = 0, .flags = .{ .linked = true } }),
727
- // Commit/rollback:
728
- std.mem.zeroInit(Account, .{ .id = 1, .flags = .{ .linked = true } }),
729
- // Fail with .exists:
730
- std.mem.zeroInit(Account, .{ .id = 0, .flags = .{ .linked = true } }),
1053
+ // Commit/rollback.
1054
+ mem.zeroInit(Account, .{ .id = 1, .code = 1, .ledger = 1, .flags = .{ .linked = true } }),
1055
+ // Commit/rollback.
1056
+ mem.zeroInit(Account, .{ .id = 2, .code = 1, .ledger = 1, .flags = .{ .linked = true } }),
1057
+ // Fail with .exists.
1058
+ mem.zeroInit(Account, .{ .id = 1, .code = 1, .ledger = 1, .flags = .{ .linked = true } }),
731
1059
  // Fail without committing.
732
- std.mem.zeroInit(Account, .{ .id = 2 }),
1060
+ mem.zeroInit(Account, .{ .id = 3, .code = 1, .ledger = 1 }),
733
1061
 
734
1062
  // An individual event (successful):
735
- // This should not see any effect from the failed chain above:
736
- std.mem.zeroInit(Account, .{ .id = 0, .code = 200 }),
1063
+ // This should not see any effect from the failed chain above.
1064
+ mem.zeroInit(Account, .{ .id = 1, .code = 1, .ledger = 1 }),
737
1065
 
738
1066
  // A chain of 2 events (the first event fails the chain):
739
- std.mem.zeroInit(Account, .{ .id = 0, .flags = .{ .linked = true } }),
740
- std.mem.zeroInit(Account, .{ .id = 1 }),
1067
+ mem.zeroInit(Account, .{ .id = 1, .code = 2, .ledger = 1, .flags = .{ .linked = true } }),
1068
+ mem.zeroInit(Account, .{ .id = 2, .code = 1, .ledger = 1 }),
741
1069
 
742
1070
  // An individual event (successful):
743
- std.mem.zeroInit(Account, .{ .id = 1, .code = 200 }),
1071
+ mem.zeroInit(Account, .{ .id = 2, .code = 1, .ledger = 1 }),
744
1072
 
745
1073
  // A chain of 2 events (the last event fails the chain):
746
- std.mem.zeroInit(Account, .{ .id = 2, .flags = .{ .linked = true } }),
747
- std.mem.zeroInit(Account, .{ .id = 0 }),
1074
+ mem.zeroInit(Account, .{ .id = 3, .code = 1, .ledger = 1, .flags = .{ .linked = true } }),
1075
+ mem.zeroInit(Account, .{ .id = 1, .code = 1, .ledger = 2 }),
748
1076
 
749
1077
  // A chain of 2 events (successful):
750
- std.mem.zeroInit(Account, .{ .id = 2, .flags = .{ .linked = true } }),
751
- std.mem.zeroInit(Account, .{ .id = 3 }),
1078
+ mem.zeroInit(Account, .{ .id = 3, .code = 1, .ledger = 1, .flags = .{ .linked = true } }),
1079
+ mem.zeroInit(Account, .{ .id = 4, .code = 1, .ledger = 1 }),
752
1080
  };
753
1081
 
754
- var state_machine = try StateMachine.init(allocator, accounts_max, transfers_max, commits_max);
1082
+ var state_machine = try StateMachine.init(
1083
+ testing.allocator,
1084
+ accounts_max,
1085
+ transfers_max,
1086
+ transfers_pending_max,
1087
+ );
755
1088
  defer state_machine.deinit();
756
1089
 
757
- const input = std.mem.asBytes(&accounts);
758
- const output = try allocator.alloc(u8, 4096);
1090
+ const input = mem.asBytes(&accounts);
1091
+
1092
+ const output = try testing.allocator.alloc(u8, 4096);
1093
+ defer testing.allocator.free(output);
759
1094
 
760
- state_machine.prepare(0, .create_accounts, input);
1095
+ _ = state_machine.prepare(.create_accounts, input);
761
1096
  const size = state_machine.commit(0, .create_accounts, input, output);
762
- const results = std.mem.bytesAsSlice(CreateAccountsResult, output[0..size]);
1097
+ const results = mem.bytesAsSlice(CreateAccountsResult, output[0..size]);
763
1098
 
764
- try testing.expectEqualSlices(
1099
+ try expectEqualSlices(
765
1100
  CreateAccountsResult,
766
1101
  &[_]CreateAccountsResult{
767
- CreateAccountsResult{ .index = 1, .result = .linked_event_failed },
768
- CreateAccountsResult{ .index = 2, .result = .linked_event_failed },
769
- CreateAccountsResult{ .index = 3, .result = .exists },
770
- CreateAccountsResult{ .index = 4, .result = .linked_event_failed },
771
-
772
- CreateAccountsResult{ .index = 6, .result = .exists_with_different_code },
773
- CreateAccountsResult{ .index = 7, .result = .linked_event_failed },
774
-
775
- CreateAccountsResult{ .index = 9, .result = .linked_event_failed },
776
- CreateAccountsResult{ .index = 10, .result = .exists_with_different_code },
1102
+ .{ .index = 1, .result = .linked_event_failed },
1103
+ .{ .index = 2, .result = .linked_event_failed },
1104
+ .{ .index = 3, .result = .exists },
1105
+ .{ .index = 4, .result = .linked_event_failed },
1106
+ .{ .index = 6, .result = .exists_with_different_flags },
1107
+ .{ .index = 7, .result = .linked_event_failed },
1108
+ .{ .index = 9, .result = .linked_event_failed },
1109
+ .{ .index = 10, .result = .exists_with_different_ledger },
777
1110
  },
778
1111
  results,
779
1112
  );
780
1113
 
781
- try testing.expectEqual(accounts[0], state_machine.get_account(accounts[0].id).?.*);
782
- try testing.expectEqual(accounts[5], state_machine.get_account(accounts[5].id).?.*);
783
- try testing.expectEqual(accounts[8], state_machine.get_account(accounts[8].id).?.*);
784
- try testing.expectEqual(accounts[11], state_machine.get_account(accounts[11].id).?.*);
785
- try testing.expectEqual(accounts[12], state_machine.get_account(accounts[12].id).?.*);
786
- try testing.expectEqual(@as(u32, 5), state_machine.accounts.count());
1114
+ try expectEqual(accounts[0], state_machine.get_account(accounts[0].id).?.*);
1115
+ try expectEqual(accounts[5], state_machine.get_account(accounts[5].id).?.*);
1116
+ try expectEqual(accounts[8], state_machine.get_account(accounts[8].id).?.*);
1117
+ try expectEqual(accounts[11], state_machine.get_account(accounts[11].id).?.*);
1118
+ try expectEqual(accounts[12], state_machine.get_account(accounts[12].id).?.*);
1119
+ try expectEqual(@as(u32, 5), state_machine.accounts.count());
787
1120
 
788
1121
  // TODO How can we test that events were in fact rolled back in LIFO order?
789
1122
  // All our rollback handlers appear to be commutative.
790
1123
  }
791
1124
 
1125
+ // The goal is to ensure that:
1126
+ // 1. all CreateTransferResult enums are covered, with
1127
+ // 2. enums tested in the order that they are defined, for easier auditing of coverage, and that
1128
+ // 3. state machine logic cannot be reordered in any way, breaking determinism.
792
1129
  test "create/lookup/rollback transfers" {
793
- var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
794
- defer arena.deinit();
795
-
796
- const allocator = arena.allocator();
797
-
798
1130
  var accounts = [_]Account{
799
- std.mem.zeroInit(Account, .{ .id = 1 }),
800
- std.mem.zeroInit(Account, .{ .id = 2 }),
801
- std.mem.zeroInit(Account, .{ .id = 3, .unit = 1 }),
802
- std.mem.zeroInit(Account, .{ .id = 4, .unit = 2 }),
803
- std.mem.zeroInit(Account, .{ .id = 5, .flags = .{ .debits_must_not_exceed_credits = true } }),
804
- std.mem.zeroInit(Account, .{ .id = 6, .flags = .{ .credits_must_not_exceed_debits = true } }),
805
- std.mem.zeroInit(Account, .{ .id = 7 }),
806
- std.mem.zeroInit(Account, .{ .id = 8 }),
1131
+ mem.zeroInit(Account, .{
1132
+ .id = 1,
1133
+ .ledger = 1,
1134
+ .code = 1,
1135
+ .debits_pending = 100,
1136
+ .debits_posted = 200,
1137
+ }),
1138
+ mem.zeroInit(Account, .{ .id = 2, .ledger = 2, .code = 2 }),
1139
+ mem.zeroInit(Account, .{
1140
+ .id = 3,
1141
+ .ledger = 1,
1142
+ .code = 1,
1143
+ .credits_pending = 110,
1144
+ .credits_posted = 210,
1145
+ }),
1146
+ mem.zeroInit(Account, .{
1147
+ .id = 4,
1148
+ .ledger = 1,
1149
+ .code = 1,
1150
+ .flags = .{ .debits_must_not_exceed_credits = true },
1151
+ .debits_pending = 20,
1152
+ .debits_posted = math.maxInt(u64) - 500 - 200,
1153
+ .credits_pending = 0,
1154
+ .credits_posted = math.maxInt(u64) - 500,
1155
+ }),
1156
+ mem.zeroInit(Account, .{
1157
+ .id = 5,
1158
+ .ledger = 1,
1159
+ .code = 1,
1160
+ .flags = .{ .credits_must_not_exceed_debits = true },
1161
+ .debits_pending = 0,
1162
+ .debits_posted = math.maxInt(u64) - 1000,
1163
+ .credits_pending = 10,
1164
+ .credits_posted = math.maxInt(u64) - 1000 - 100,
1165
+ }),
807
1166
  };
808
1167
 
809
- var state_machine = try StateMachine.init(allocator, accounts.len, 1, 0);
1168
+ var state_machine = try StateMachine.init(testing.allocator, accounts.len, 1, 0);
810
1169
  defer state_machine.deinit();
811
1170
 
812
- const input = std.mem.asBytes(&accounts);
813
- const output = try allocator.alloc(u8, 4096);
1171
+ const input = mem.asBytes(&accounts);
814
1172
 
815
- state_machine.prepare(0, .create_accounts, input);
1173
+ const output = try testing.allocator.alloc(u8, 4096);
1174
+ defer testing.allocator.free(output);
1175
+
1176
+ _ = state_machine.prepare(.create_accounts, input);
816
1177
  const size = state_machine.commit(0, .create_accounts, input, output);
817
1178
 
818
- const errors = std.mem.bytesAsSlice(CreateAccountsResult, output[0..size]);
819
- try testing.expectEqual(@as(usize, 0), errors.len);
1179
+ const errors = mem.bytesAsSlice(CreateAccountsResult, output[0..size]);
1180
+ try expect(errors.len == 0);
820
1181
 
821
1182
  for (accounts) |account| {
822
- try testing.expectEqual(account, state_machine.get_account(account.id).?.*);
1183
+ try expectEqual(account, state_machine.get_account(account.id).?.*);
823
1184
  }
824
1185
 
825
1186
  const Vector = struct { result: CreateTransferResult, object: Transfer };
826
1187
 
827
1188
  const timestamp: u64 = (state_machine.commit_timestamp + 1);
828
1189
  const vectors = [_]Vector{
829
- Vector{
830
- .result = .amount_is_zero,
831
- .object = std.mem.zeroInit(Transfer, .{
1190
+ .{
1191
+ .result = .reserved_flag,
1192
+ .object = mem.zeroInit(Transfer, .{
1193
+ .id = 0,
1194
+ .debit_account_id = 0,
1195
+ .credit_account_id = 0,
1196
+ .reserved = 1,
1197
+ .pending_id = 1,
1198
+ .timeout = 0,
1199
+ .ledger = 0,
1200
+ .code = 0,
1201
+ .flags = .{ .pending = true, .padding = 1 },
1202
+ .amount = 0,
1203
+ .timestamp = timestamp,
1204
+ }),
1205
+ },
1206
+ .{
1207
+ .result = .reserved_field,
1208
+ .object = mem.zeroInit(Transfer, .{
1209
+ .id = 0,
1210
+ .debit_account_id = 0,
1211
+ .credit_account_id = 0,
1212
+ .reserved = 1,
1213
+ .pending_id = 1,
1214
+ .timeout = 0,
1215
+ .ledger = 0,
1216
+ .code = 0,
1217
+ .flags = .{ .pending = true },
1218
+ .amount = 0,
1219
+ .timestamp = timestamp,
1220
+ }),
1221
+ },
1222
+ .{
1223
+ .result = .id_must_not_be_zero,
1224
+ .object = mem.zeroInit(Transfer, .{
1225
+ .id = 0,
1226
+ .debit_account_id = 0,
1227
+ .credit_account_id = 0,
1228
+ .pending_id = 1,
1229
+ .timeout = 0,
1230
+ .ledger = 0,
1231
+ .code = 0,
1232
+ .flags = .{ .pending = true },
1233
+ .amount = 0,
1234
+ .timestamp = timestamp,
1235
+ }),
1236
+ },
1237
+ .{
1238
+ .result = .debit_account_id_must_not_be_zero,
1239
+ .object = mem.zeroInit(Transfer, .{
832
1240
  .id = 1,
1241
+ .debit_account_id = 0,
1242
+ .credit_account_id = 0,
1243
+ .pending_id = 1,
1244
+ .timeout = 0,
1245
+ .ledger = 0,
1246
+ .code = 0,
1247
+ .flags = .{ .pending = true },
1248
+ .amount = 0,
833
1249
  .timestamp = timestamp,
834
1250
  }),
835
1251
  },
836
- Vector{
837
- .result = .reserved_flag_padding,
838
- .object = std.mem.zeroInit(Transfer, .{
839
- .id = 2,
1252
+ .{
1253
+ .result = .credit_account_id_must_not_be_zero,
1254
+ .object = mem.zeroInit(Transfer, .{
1255
+ .id = 1,
1256
+ .debit_account_id = 100,
1257
+ .credit_account_id = 0,
1258
+ .pending_id = 1,
1259
+ .timeout = 0,
1260
+ .ledger = 0,
1261
+ .code = 0,
1262
+ .flags = .{ .pending = true },
1263
+ .amount = 0,
840
1264
  .timestamp = timestamp,
841
- .flags = .{ .padding = 1 },
842
1265
  }),
843
1266
  },
844
- Vector{
845
- .result = .two_phase_commit_must_timeout,
846
- .object = std.mem.zeroInit(Transfer, .{
847
- .id = 3,
1267
+ .{
1268
+ .result = .accounts_must_be_different,
1269
+ .object = mem.zeroInit(Transfer, .{
1270
+ .id = 1,
1271
+ .debit_account_id = 100,
1272
+ .credit_account_id = 100,
1273
+ .pending_id = 1,
1274
+ .timeout = 0,
1275
+ .ledger = 0,
1276
+ .code = 0,
1277
+ .flags = .{ .pending = true },
1278
+ .amount = 0,
848
1279
  .timestamp = timestamp,
849
- .flags = .{ .two_phase_commit = true },
850
1280
  }),
851
1281
  },
852
- Vector{
853
- .result = .timeout_reserved_for_two_phase_commit,
854
- .object = std.mem.zeroInit(Transfer, .{
855
- .id = 4,
1282
+ .{
1283
+ .result = .pending_id_must_be_zero,
1284
+ .object = mem.zeroInit(Transfer, .{
1285
+ .id = 1,
1286
+ .debit_account_id = 100,
1287
+ .credit_account_id = 200,
1288
+ .pending_id = 1,
1289
+ .timeout = 0,
1290
+ .ledger = 0,
1291
+ .code = 0,
1292
+ .flags = .{ .pending = true },
1293
+ .amount = 0,
856
1294
  .timestamp = timestamp,
1295
+ }),
1296
+ },
1297
+ .{
1298
+ .result = .pending_transfer_must_timeout,
1299
+ .object = mem.zeroInit(Transfer, .{
1300
+ .id = 1,
1301
+ .debit_account_id = 100,
1302
+ .credit_account_id = 200,
1303
+ .timeout = 0,
1304
+ .ledger = 0,
1305
+ .code = 0,
1306
+ .flags = .{ .pending = true },
1307
+ .amount = 0,
1308
+ .timestamp = timestamp,
1309
+ }),
1310
+ },
1311
+ .{
1312
+ .result = .timeout_reserved_for_pending_transfer,
1313
+ .object = mem.zeroInit(Transfer, .{
1314
+ .id = 1,
1315
+ .debit_account_id = 100,
1316
+ .credit_account_id = 200,
857
1317
  .timeout = 1,
1318
+ .ledger = 0,
1319
+ .code = 0,
1320
+ .amount = 0,
1321
+ .timestamp = timestamp,
858
1322
  }),
859
1323
  },
860
- Vector{
861
- .result = .reserved_field,
862
- .object = std.mem.zeroInit(Transfer, .{
863
- .id = 5,
1324
+ .{
1325
+ .result = .ledger_must_not_be_zero,
1326
+ .object = mem.zeroInit(Transfer, .{
1327
+ .id = 1,
1328
+ .debit_account_id = 100,
1329
+ .credit_account_id = 200,
1330
+ .timeout = 1,
1331
+ .ledger = 0,
1332
+ .code = 0,
1333
+ .flags = .{ .pending = true },
1334
+ .amount = 0,
864
1335
  .timestamp = timestamp,
865
- .flags = .{ .condition = false },
866
- .reserved = [_]u8{1} ** 32,
867
1336
  }),
868
1337
  },
869
- Vector{
870
- .result = .accounts_are_the_same,
871
- .object = std.mem.zeroInit(Transfer, .{
872
- .id = 6,
1338
+ .{
1339
+ .result = .code_must_not_be_zero,
1340
+ .object = mem.zeroInit(Transfer, .{
1341
+ .id = 1,
1342
+ .debit_account_id = 100,
1343
+ .credit_account_id = 200,
1344
+ .timeout = 1,
1345
+ .ledger = 100,
1346
+ .code = 0,
1347
+ .flags = .{ .pending = true },
1348
+ .amount = 0,
873
1349
  .timestamp = timestamp,
874
- .amount = 10,
875
- .debit_account_id = 1,
876
- .credit_account_id = 1,
877
1350
  }),
878
1351
  },
879
- Vector{
880
- .result = .debit_account_not_found,
881
- .object = std.mem.zeroInit(Transfer, .{
882
- .id = 7,
1352
+ .{
1353
+ .result = .amount_must_not_be_zero,
1354
+ .object = mem.zeroInit(Transfer, .{
1355
+ .id = 1,
1356
+ .debit_account_id = 100,
1357
+ .credit_account_id = 200,
1358
+ .timeout = 1,
1359
+ .ledger = 100,
1360
+ .code = 1,
1361
+ .flags = .{ .pending = true },
1362
+ .amount = 0,
883
1363
  .timestamp = timestamp,
884
- .amount = 10,
1364
+ }),
1365
+ },
1366
+ .{
1367
+ .result = .debit_account_not_found,
1368
+ .object = mem.zeroInit(Transfer, .{
1369
+ .id = 1,
885
1370
  .debit_account_id = 100,
886
- .credit_account_id = 1,
1371
+ .credit_account_id = 200,
1372
+ .timeout = 1,
1373
+ .ledger = 100,
1374
+ .code = 1,
1375
+ .flags = .{ .pending = true },
1376
+ .amount = 100,
1377
+ .timestamp = timestamp,
887
1378
  }),
888
1379
  },
889
- Vector{
1380
+ .{
890
1381
  .result = .credit_account_not_found,
891
- .object = std.mem.zeroInit(Transfer, .{
892
- .id = 8,
1382
+ .object = mem.zeroInit(Transfer, .{
1383
+ .id = 1,
1384
+ .debit_account_id = 1,
1385
+ .credit_account_id = 200,
1386
+ .timeout = 1,
1387
+ .ledger = 100,
1388
+ .code = 1,
1389
+ .flags = .{ .pending = true },
1390
+ .amount = 100,
893
1391
  .timestamp = timestamp,
894
- .amount = 10,
1392
+ }),
1393
+ },
1394
+ .{
1395
+ .result = .accounts_must_have_the_same_ledger,
1396
+ .object = mem.zeroInit(Transfer, .{
1397
+ .id = 1,
895
1398
  .debit_account_id = 1,
896
- .credit_account_id = 100,
1399
+ .credit_account_id = 2,
1400
+ .ledger = 100,
1401
+ .code = 1,
1402
+ .amount = 1,
1403
+ .timestamp = timestamp,
897
1404
  }),
898
1405
  },
899
- Vector{
900
- .result = .accounts_have_different_units,
901
- .object = std.mem.zeroInit(Transfer, .{
902
- .id = 9,
1406
+ .{
1407
+ .result = .transfer_must_have_the_same_ledger_as_accounts,
1408
+ .object = mem.zeroInit(Transfer, .{
1409
+ .id = 1,
1410
+ .debit_account_id = 1,
1411
+ .credit_account_id = 3,
1412
+ .ledger = 100,
1413
+ .code = 1,
1414
+ .amount = 1,
903
1415
  .timestamp = timestamp,
904
- .amount = 10,
905
- .debit_account_id = 3,
906
- .credit_account_id = 4,
907
1416
  }),
908
1417
  },
909
- Vector{
910
- .result = .exceeds_credits,
911
- .object = std.mem.zeroInit(Transfer, .{
912
- .id = 10,
1418
+ .{
1419
+ .result = .overflows_debits_pending,
1420
+ .object = mem.zeroInit(Transfer, .{
1421
+ .id = 1,
1422
+ .debit_account_id = 1,
1423
+ .credit_account_id = 3,
1424
+ .timeout = 30000,
1425
+ .ledger = 1,
1426
+ .code = 1,
1427
+ .flags = .{ .pending = true },
1428
+ .amount = math.maxInt(u64) - accounts[1 - 1].debits_pending + 1,
913
1429
  .timestamp = timestamp,
914
- .amount = 1000,
915
- .debit_account_id = 5,
916
- .credit_account_id = 1,
917
1430
  }),
918
1431
  },
919
- Vector{
920
- .result = .exceeds_debits,
921
- .object = std.mem.zeroInit(Transfer, .{
922
- .id = 11,
1432
+ .{
1433
+ .result = .overflows_credits_pending,
1434
+ .object = mem.zeroInit(Transfer, .{
1435
+ .id = 1,
1436
+ .debit_account_id = 1,
1437
+ .credit_account_id = 3,
1438
+ .timeout = 30000,
1439
+ .ledger = 1,
1440
+ .code = 1,
1441
+ .flags = .{ .pending = true },
1442
+ .amount = math.maxInt(u64) - accounts[3 - 1].credits_pending + 1,
923
1443
  .timestamp = timestamp,
924
- .amount = 1000,
1444
+ }),
1445
+ },
1446
+ .{
1447
+ .result = .overflows_debits_posted,
1448
+ .object = mem.zeroInit(Transfer, .{
1449
+ .id = 1,
925
1450
  .debit_account_id = 1,
926
- .credit_account_id = 6,
1451
+ .credit_account_id = 3,
1452
+ .ledger = 1,
1453
+ .code = 1,
1454
+ .amount = math.maxInt(u64) - accounts[1 - 1].debits_posted + 1,
1455
+ .timestamp = timestamp,
927
1456
  }),
928
1457
  },
929
- Vector{
930
- .result = .ok,
931
- .object = std.mem.zeroInit(Transfer, .{
932
- .id = 12,
1458
+ .{
1459
+ .result = .overflows_credits_posted,
1460
+ .object = mem.zeroInit(Transfer, .{
1461
+ .id = 1,
1462
+ .debit_account_id = 1,
1463
+ .credit_account_id = 3,
1464
+ .ledger = 1,
1465
+ .code = 1,
1466
+ .amount = math.maxInt(u64) - accounts[3 - 1].credits_posted + 1,
933
1467
  .timestamp = timestamp,
934
- .amount = 10,
935
- .debit_account_id = 7,
936
- .credit_account_id = 8,
937
1468
  }),
938
1469
  },
939
- Vector{
940
- .result = .exists,
941
- .object = std.mem.zeroInit(Transfer, .{
942
- .id = 12,
943
- .timestamp = timestamp + 1,
944
- .amount = 10,
945
- .debit_account_id = 7,
946
- .credit_account_id = 8,
1470
+ .{
1471
+ .result = .overflows_debits,
1472
+ .object = mem.zeroInit(Transfer, .{
1473
+ .id = 1,
1474
+ .debit_account_id = 1,
1475
+ .credit_account_id = 3,
1476
+ .ledger = 1,
1477
+ .code = 1,
1478
+ .amount = math.maxInt(u64) -
1479
+ accounts[1 - 1].debits_pending -
1480
+ accounts[1 - 1].debits_posted + 1,
1481
+ .timestamp = timestamp,
947
1482
  }),
948
1483
  },
949
- Vector{
950
- .result = .exists_with_different_debit_account_id,
951
- .object = std.mem.zeroInit(Transfer, .{
952
- .id = 12,
953
- .timestamp = timestamp + 1,
954
- .amount = 10,
955
- .debit_account_id = 8,
956
- .credit_account_id = 7,
1484
+ .{
1485
+ .result = .overflows_credits,
1486
+ .object = mem.zeroInit(Transfer, .{
1487
+ .id = 1,
1488
+ .debit_account_id = 1,
1489
+ .credit_account_id = 3,
1490
+ .ledger = 1,
1491
+ .code = 1,
1492
+ .amount = math.maxInt(u64) -
1493
+ accounts[3 - 1].credits_pending -
1494
+ accounts[3 - 1].credits_posted + 1,
1495
+ .timestamp = timestamp,
957
1496
  }),
958
1497
  },
959
- Vector{
960
- .result = .exists_with_different_credit_account_id,
961
- .object = std.mem.zeroInit(Transfer, .{
962
- .id = 12,
963
- .timestamp = timestamp + 1,
964
- .amount = 10,
965
- .debit_account_id = 7,
966
- .credit_account_id = 1,
1498
+ .{
1499
+ .result = .exceeds_credits,
1500
+ .object = mem.zeroInit(Transfer, .{
1501
+ .id = 1,
1502
+ .debit_account_id = 4,
1503
+ .credit_account_id = 5,
1504
+ .ledger = 1,
1505
+ .code = 1,
1506
+ .amount = accounts[4 - 1].credits_posted -
1507
+ accounts[4 - 1].debits_pending -
1508
+ accounts[4 - 1].debits_posted + 1,
1509
+ .timestamp = timestamp,
967
1510
  }),
968
1511
  },
969
- Vector{
970
- .result = .exists_with_different_amount,
971
- .object = std.mem.zeroInit(Transfer, .{
972
- .id = 12,
1512
+ .{
1513
+ .result = .exceeds_debits,
1514
+ .object = mem.zeroInit(Transfer, .{
1515
+ .id = 1,
1516
+ .debit_account_id = 4,
1517
+ .credit_account_id = 5,
1518
+ .ledger = 1,
1519
+ .code = 1,
1520
+ .amount = accounts[5 - 1].debits_posted -
1521
+ accounts[5 - 1].credits_pending -
1522
+ accounts[5 - 1].credits_posted + 1,
1523
+ .timestamp = timestamp,
1524
+ }),
1525
+ },
1526
+ .{
1527
+ .result = .ok,
1528
+ .object = mem.zeroInit(Transfer, .{
1529
+ .id = 1,
1530
+ .debit_account_id = 1,
1531
+ .credit_account_id = 3,
1532
+ .timeout = 10000,
1533
+ .ledger = 1,
1534
+ .code = 1,
1535
+ .flags = .{ .pending = true },
1536
+ .amount = 123,
973
1537
  .timestamp = timestamp + 1,
974
- .amount = 11,
975
- .debit_account_id = 7,
976
- .credit_account_id = 8,
977
1538
  }),
978
1539
  },
979
- Vector{
1540
+ .{
1541
+ // Ensure that idempotence is only checked after validation.
1542
+ .result = .transfer_must_have_the_same_ledger_as_accounts,
1543
+ .object = mem.zeroInit(Transfer, .{
1544
+ .id = 1,
1545
+ .debit_account_id = 1,
1546
+ .credit_account_id = 3,
1547
+ .timeout = 10000,
1548
+ .ledger = 2,
1549
+ .code = 1,
1550
+ .flags = .{ .pending = true },
1551
+ .amount = 123,
1552
+ .timestamp = timestamp + 2,
1553
+ }),
1554
+ },
1555
+ .{
980
1556
  .result = .exists_with_different_flags,
981
- .object = std.mem.zeroInit(Transfer, .{
982
- .id = 12,
983
- .timestamp = timestamp + 1,
984
- .amount = 10,
985
- .debit_account_id = 7,
986
- .credit_account_id = 8,
987
- .flags = .{ .condition = true },
1557
+ .object = mem.zeroInit(Transfer, .{
1558
+ .id = 1,
1559
+ .debit_account_id = 1,
1560
+ .credit_account_id = 3,
1561
+ .user_data = 1,
1562
+ .ledger = 1,
1563
+ .code = 2,
1564
+ .flags = .{ .pending = false },
1565
+ .amount = math.maxInt(u64),
1566
+ .timestamp = timestamp + 2,
988
1567
  }),
989
1568
  },
990
- Vector{
1569
+ .{
1570
+ .result = .exists_with_different_debit_account_id,
1571
+ .object = mem.zeroInit(Transfer, .{
1572
+ .id = 1,
1573
+ .debit_account_id = 3,
1574
+ .credit_account_id = 1,
1575
+ .user_data = 1,
1576
+ .timeout = 10000,
1577
+ .ledger = 1,
1578
+ .code = 2,
1579
+ .flags = .{ .pending = true },
1580
+ .amount = math.maxInt(u64),
1581
+ .timestamp = timestamp + 2,
1582
+ }),
1583
+ },
1584
+ .{
1585
+ .result = .exists_with_different_credit_account_id,
1586
+ .object = mem.zeroInit(Transfer, .{
1587
+ .id = 1,
1588
+ .debit_account_id = 1,
1589
+ .credit_account_id = 4,
1590
+ .user_data = 1,
1591
+ .timeout = 10000,
1592
+ .ledger = 1,
1593
+ .code = 2,
1594
+ .flags = .{ .pending = true },
1595
+ .amount = math.maxInt(u64),
1596
+ .timestamp = timestamp + 2,
1597
+ }),
1598
+ },
1599
+ .{
991
1600
  .result = .exists_with_different_user_data,
992
- .object = std.mem.zeroInit(Transfer, .{
993
- .id = 12,
994
- .timestamp = timestamp + 1,
995
- .amount = 10,
996
- .debit_account_id = 7,
997
- .credit_account_id = 8,
998
- .user_data = 'A',
1601
+ .object = mem.zeroInit(Transfer, .{
1602
+ .id = 1,
1603
+ .debit_account_id = 1,
1604
+ .credit_account_id = 3,
1605
+ .user_data = 1,
1606
+ .timeout = 10000,
1607
+ .ledger = 1,
1608
+ .code = 2,
1609
+ .flags = .{ .pending = true },
1610
+ .amount = math.maxInt(u64),
1611
+ .timestamp = timestamp + 2,
999
1612
  }),
1000
1613
  },
1001
- Vector{
1002
- .result = .ok,
1003
- .object = std.mem.zeroInit(Transfer, .{
1004
- .id = 13,
1005
- .timestamp = timestamp + 1,
1006
- .amount = 10,
1007
- .debit_account_id = 7,
1008
- .credit_account_id = 8,
1009
- .flags = .{ .condition = true },
1010
- .reserved = [_]u8{1} ** 32,
1614
+ .{
1615
+ .result = .exists_with_different_timeout,
1616
+ .object = mem.zeroInit(Transfer, .{
1617
+ .id = 1,
1618
+ .debit_account_id = 1,
1619
+ .credit_account_id = 3,
1620
+ .timeout = 10001,
1621
+ .ledger = 1,
1622
+ .code = 2,
1623
+ .flags = .{ .pending = true },
1624
+ .amount = math.maxInt(u64),
1625
+ .timestamp = timestamp + 2,
1011
1626
  }),
1012
1627
  },
1013
- Vector{
1014
- .result = .exists_with_different_reserved_field,
1015
- .object = std.mem.zeroInit(Transfer, .{
1016
- .id = 13,
1628
+ .{
1629
+ .result = .exists_with_different_code,
1630
+ .object = mem.zeroInit(Transfer, .{
1631
+ .id = 1,
1632
+ .debit_account_id = 1,
1633
+ .credit_account_id = 3,
1634
+ .timeout = 10000,
1635
+ .ledger = 1,
1636
+ .code = 2,
1637
+ .flags = .{ .pending = true },
1638
+ .amount = math.maxInt(u64),
1017
1639
  .timestamp = timestamp + 2,
1018
- .amount = 10,
1019
- .debit_account_id = 7,
1020
- .credit_account_id = 8,
1021
- .flags = .{ .condition = true },
1022
- .reserved = [_]u8{2} ** 32,
1023
1640
  }),
1024
1641
  },
1025
- Vector{
1026
- .result = .timeout_reserved_for_two_phase_commit,
1027
- .object = std.mem.zeroInit(Transfer, .{
1028
- .id = 13,
1642
+ .{
1643
+ .result = .exists_with_different_amount,
1644
+ .object = mem.zeroInit(Transfer, .{
1645
+ .id = 1,
1646
+ .debit_account_id = 1,
1647
+ .credit_account_id = 3,
1648
+ .timeout = 10000,
1649
+ .ledger = 1,
1650
+ .code = 1,
1651
+ .flags = .{ .pending = true },
1652
+ .amount = math.maxInt(u64),
1029
1653
  .timestamp = timestamp + 2,
1030
- .amount = 10,
1031
- .debit_account_id = 7,
1032
- .credit_account_id = 8,
1033
- .flags = .{ .condition = true },
1034
- .reserved = [_]u8{1} ** 32,
1035
- .timeout = 10,
1036
- }),
1037
- },
1038
- Vector{
1039
- .result = .two_phase_commit_must_timeout,
1040
- .object = std.mem.zeroInit(Transfer, .{
1041
- .id = 14,
1654
+ }),
1655
+ },
1656
+ .{
1657
+ .result = .exists,
1658
+ .object = mem.zeroInit(Transfer, .{
1659
+ .id = 1,
1660
+ .debit_account_id = 1,
1661
+ .credit_account_id = 3,
1662
+ .timeout = 10000,
1663
+ .ledger = 1,
1664
+ .code = 1,
1665
+ .flags = .{ .pending = true },
1666
+ .amount = 123,
1042
1667
  .timestamp = timestamp + 2,
1043
- .amount = 10,
1044
- .debit_account_id = 7,
1045
- .credit_account_id = 8,
1046
- .flags = .{ .two_phase_commit = true },
1047
- .timeout = 0,
1048
1668
  }),
1049
1669
  },
1050
- Vector{
1670
+ .{
1051
1671
  .result = .ok,
1052
- .object = std.mem.zeroInit(Transfer, .{
1053
- .id = 15,
1672
+ .object = mem.zeroInit(Transfer, .{
1673
+ .id = 2,
1674
+ .debit_account_id = 3,
1675
+ .credit_account_id = 1,
1676
+ .ledger = 1,
1677
+ .code = 2,
1678
+ .amount = 7,
1054
1679
  .timestamp = timestamp + 2,
1055
- .amount = 10,
1056
- .debit_account_id = 7,
1057
- .credit_account_id = 8,
1058
- .flags = .{ .two_phase_commit = true },
1059
- .timeout = 20,
1060
1680
  }),
1061
1681
  },
1062
- Vector{
1063
- .result = .exists_with_different_timeout,
1064
- .object = std.mem.zeroInit(Transfer, .{
1065
- .id = 15,
1682
+ .{
1683
+ .result = .ok,
1684
+ .object = mem.zeroInit(Transfer, .{
1685
+ .id = 3,
1686
+ .debit_account_id = 1,
1687
+ .credit_account_id = 3,
1688
+ .ledger = 1,
1689
+ .code = 2,
1690
+ .amount = 3,
1066
1691
  .timestamp = timestamp + 3,
1067
- .amount = 10,
1068
- .debit_account_id = 7,
1069
- .credit_account_id = 8,
1070
- .flags = .{ .two_phase_commit = true },
1071
- .timeout = 25,
1072
1692
  }),
1073
1693
  },
1074
1694
  };
1075
1695
 
1076
- for (vectors) |vector| {
1077
- try testing.expectEqual(vector.result, state_machine.create_transfer(vector.object));
1696
+ for (vectors) |vector, i| {
1697
+ const result = state_machine.create_transfer(&vector.object);
1698
+ expectEqual(vector.result, result) catch |err| {
1699
+ print_test_vector(i, vector.result, result, vector.object, err);
1700
+ return err;
1701
+ };
1078
1702
  if (vector.result == .ok) {
1079
- try testing.expectEqual(vector.object, state_machine.get_transfer(vector.object.id).?.*);
1703
+ try expectEqual(vector.object, state_machine.get_transfer(vector.object.id).?.*);
1080
1704
  }
1081
1705
  }
1082
1706
 
1083
- // 2 phase commit [reserved]:
1084
- try testing.expectEqual(@as(u64, 10), state_machine.get_account(7).?.*.debits_reserved);
1085
- try testing.expectEqual(@as(u64, 0), state_machine.get_account(7).?.*.credits_reserved);
1086
- try testing.expectEqual(@as(u64, 10), state_machine.get_account(8).?.*.credits_reserved);
1087
- try testing.expectEqual(@as(u64, 0), state_machine.get_account(8).?.*.debits_reserved);
1088
- // 1 phase commit [accepted]:
1089
- try testing.expectEqual(@as(u64, 20), state_machine.get_account(7).?.*.debits_accepted);
1090
- try testing.expectEqual(@as(u64, 0), state_machine.get_account(7).?.*.credits_accepted);
1091
- try testing.expectEqual(@as(u64, 20), state_machine.get_account(8).?.*.credits_accepted);
1092
- try testing.expectEqual(@as(u64, 0), state_machine.get_account(8).?.*.debits_accepted);
1093
-
1094
- // Rollback transfer with id [12], amount of 10:
1095
- state_machine.create_transfer_rollback(state_machine.get_transfer(vectors[11].object.id).?.*);
1096
- try testing.expectEqual(@as(u64, 10), state_machine.get_account(7).?.*.debits_accepted);
1097
- try testing.expectEqual(@as(u64, 0), state_machine.get_account(7).?.*.credits_accepted);
1098
- try testing.expectEqual(@as(u64, 10), state_machine.get_account(8).?.*.credits_accepted);
1099
- try testing.expectEqual(@as(u64, 0), state_machine.get_account(8).?.*.debits_accepted);
1100
- try testing.expect(state_machine.get_transfer(vectors[11].object.id) == null);
1101
-
1102
- // Rollback transfer with id [15], amount of 10:
1103
- state_machine.create_transfer_rollback(state_machine.get_transfer(vectors[22].object.id).?.*);
1104
- try testing.expectEqual(@as(u64, 0), state_machine.get_account(7).?.*.debits_reserved);
1105
- try testing.expectEqual(@as(u64, 0), state_machine.get_account(8).?.*.credits_reserved);
1106
- try testing.expect(state_machine.get_transfer(vectors[22].object.id) == null);
1107
- }
1707
+ // Transfer 3:
1708
+ try test_account_balances(&state_machine, 1, 100 + 123, 200 + 3, 0, 7);
1709
+ try test_account_balances(&state_machine, 3, 0, 7, 110 + 123, 210 + 3);
1710
+ state_machine.create_transfer_rollback(state_machine.get_transfer(3).?);
1711
+ try test_account_balances(&state_machine, 1, 100 + 123, 200, 0, 7);
1712
+ try test_account_balances(&state_machine, 3, 0, 7, 110 + 123, 210);
1713
+ try expect(state_machine.get_transfer(3) == null);
1714
+
1715
+ // Transfer 2:
1716
+ try test_account_balances(&state_machine, 1, 100 + 123, 200, 0, 7);
1717
+ try test_account_balances(&state_machine, 3, 0, 7, 110 + 123, 210);
1718
+ state_machine.create_transfer_rollback(state_machine.get_transfer(2).?);
1719
+ try test_account_balances(&state_machine, 1, 100 + 123, 200, 0, 0);
1720
+ try test_account_balances(&state_machine, 3, 0, 0, 110 + 123, 210);
1721
+ try expect(state_machine.get_transfer(2) == null);
1722
+
1723
+ // Transfer 1:
1724
+ try test_account_balances(&state_machine, 1, 100 + 123, 200, 0, 0);
1725
+ try test_account_balances(&state_machine, 3, 0, 0, 110 + 123, 210);
1726
+ state_machine.create_transfer_rollback(state_machine.get_transfer(1).?);
1727
+ try test_account_balances(&state_machine, 1, 100, 200, 0, 0);
1728
+ try test_account_balances(&state_machine, 3, 0, 0, 110, 210);
1729
+ try expect(state_machine.get_transfer(1) == null);
1108
1730
 
1109
- test "create/lookup/rollback commits" {
1110
- var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
1111
- defer arena.deinit();
1112
-
1113
- const allocator = arena.allocator();
1114
-
1115
- const Vector = struct { result: CommitTransferResult, object: Commit };
1731
+ for (accounts) |account| {
1732
+ state_machine.create_account_rollback(&account);
1733
+ try expect(state_machine.get_account(account.id) == null);
1734
+ }
1735
+ }
1116
1736
 
1737
+ test "create/lookup/rollback 2-phase transfers" {
1117
1738
  var accounts = [_]Account{
1118
- std.mem.zeroInit(Account, .{ .id = 1 }),
1119
- std.mem.zeroInit(Account, .{ .id = 2 }),
1120
- std.mem.zeroInit(Account, .{ .id = 3 }),
1121
- std.mem.zeroInit(Account, .{ .id = 4 }),
1739
+ mem.zeroInit(Account, .{ .id = 1, .ledger = 1, .code = 1 }),
1740
+ mem.zeroInit(Account, .{ .id = 2, .ledger = 1, .code = 1 }),
1122
1741
  };
1123
1742
 
1124
1743
  var transfers = [_]Transfer{
1125
- std.mem.zeroInit(Transfer, .{
1744
+ mem.zeroInit(Transfer, .{
1126
1745
  .id = 1,
1127
- .amount = 15,
1128
1746
  .debit_account_id = 1,
1129
1747
  .credit_account_id = 2,
1748
+ .ledger = 1,
1749
+ .code = 1,
1750
+ .amount = 15,
1130
1751
  }),
1131
- std.mem.zeroInit(Transfer, .{
1752
+ mem.zeroInit(Transfer, .{
1132
1753
  .id = 2,
1133
- .amount = 15,
1134
1754
  .debit_account_id = 1,
1135
1755
  .credit_account_id = 2,
1136
- .flags = .{ .two_phase_commit = true },
1137
- .timeout = 25,
1756
+ .timeout = 1000,
1757
+ .ledger = 1,
1758
+ .code = 1,
1759
+ .flags = .{ .pending = true },
1760
+ .amount = 15,
1138
1761
  }),
1139
- std.mem.zeroInit(Transfer, .{
1762
+ mem.zeroInit(Transfer, .{
1140
1763
  .id = 3,
1141
- .amount = 15,
1142
1764
  .debit_account_id = 1,
1143
1765
  .credit_account_id = 2,
1144
- .flags = .{ .two_phase_commit = true },
1145
- .timeout = 25,
1766
+ .timeout = 5,
1767
+ .ledger = 1,
1768
+ .code = 1,
1769
+ .flags = .{ .pending = true },
1770
+ .amount = 15,
1146
1771
  }),
1147
- std.mem.zeroInit(Transfer, .{
1772
+ mem.zeroInit(Transfer, .{
1148
1773
  .id = 4,
1149
- .amount = 15,
1150
1774
  .debit_account_id = 1,
1151
1775
  .credit_account_id = 2,
1152
- .flags = .{ .two_phase_commit = true },
1153
1776
  .timeout = 1,
1154
- }),
1155
- std.mem.zeroInit(Transfer, .{
1156
- .id = 5,
1777
+ .ledger = 1,
1778
+ .code = 1,
1779
+ .flags = .{ .pending = true },
1157
1780
  .amount = 15,
1158
- .debit_account_id = 1,
1159
- .credit_account_id = 2,
1160
- .flags = .{
1161
- .two_phase_commit = true,
1162
- .condition = true,
1163
- },
1164
- .timeout = 25,
1165
1781
  }),
1166
- std.mem.zeroInit(Transfer, .{
1167
- .id = 6,
1168
- .amount = 15,
1782
+ mem.zeroInit(Transfer, .{
1783
+ .id = 5,
1169
1784
  .debit_account_id = 1,
1170
1785
  .credit_account_id = 2,
1171
- .flags = .{
1172
- .two_phase_commit = true,
1173
- .condition = false,
1174
- },
1175
- .timeout = 25,
1176
- }),
1177
- std.mem.zeroInit(Transfer, .{
1178
- .id = 7,
1179
- .amount = 15,
1180
- .debit_account_id = 3,
1181
- .credit_account_id = 4,
1182
- .flags = .{ .two_phase_commit = true },
1183
- .timeout = 25,
1786
+ .user_data = 73,
1787
+ .timeout = 6,
1788
+ .ledger = 1,
1789
+ .code = 1,
1790
+ .flags = .{ .pending = true },
1791
+ .amount = 7,
1184
1792
  }),
1185
1793
  };
1186
1794
 
1187
- var state_machine = try StateMachine.init(allocator, accounts.len, transfers.len, 1);
1795
+ var state_machine = try StateMachine.init(testing.allocator, accounts.len, 100, 1);
1188
1796
  defer state_machine.deinit();
1189
1797
 
1190
- const input = std.mem.asBytes(&accounts);
1191
- const output = try allocator.alloc(u8, 4096);
1798
+ // Create accounts:
1799
+ const accounts_input = mem.asBytes(&accounts);
1192
1800
 
1193
- // Accounts:
1194
- state_machine.prepare(0, .create_accounts, input);
1195
- const size = state_machine.commit(0, .create_accounts, input, output);
1801
+ const accounts_output = try testing.allocator.alloc(u8, 4096);
1802
+ defer testing.allocator.free(accounts_output);
1803
+
1804
+ const accounts_timestamp = state_machine.prepare(.create_accounts, accounts_input);
1196
1805
  {
1197
- const errors = std.mem.bytesAsSlice(CreateAccountsResult, output[0..size]);
1198
- try testing.expectEqual(@as(usize, 0), errors.len);
1806
+ const size = state_machine.commit(0, .create_accounts, accounts_input, accounts_output);
1807
+ const errors = mem.bytesAsSlice(CreateAccountsResult, accounts_output[0..size]);
1808
+ try expectEqual(@as(usize, 0), errors.len);
1199
1809
  }
1200
-
1201
1810
  for (accounts) |account| {
1202
- try testing.expectEqual(account, state_machine.get_account(account.id).?.*);
1811
+ try expectEqual(account, state_machine.get_account(account.id).?.*);
1203
1812
  }
1204
1813
 
1205
- // Transfers:
1206
- const object_transfers = std.mem.asBytes(&transfers);
1207
- const output_transfers = try allocator.alloc(u8, 4096);
1814
+ // Create pending transfers:
1815
+ const transfers_input = mem.asBytes(&transfers);
1208
1816
 
1209
- state_machine.prepare(0, .create_transfers, object_transfers);
1210
- const size_transfers = state_machine.commit(
1211
- 0,
1212
- .create_transfers,
1213
- object_transfers,
1214
- output_transfers,
1215
- );
1216
- const errors = std.mem.bytesAsSlice(CreateTransfersResult, output_transfers[0..size_transfers]);
1217
- try testing.expectEqual(@as(usize, 0), errors.len);
1817
+ const transfers_output = try testing.allocator.alloc(u8, 4096);
1818
+ defer testing.allocator.free(transfers_output);
1218
1819
 
1820
+ const transfers_timestamp = state_machine.prepare(.create_transfers, transfers_input);
1821
+ try testing.expect(transfers_timestamp > accounts_timestamp);
1822
+ {
1823
+ const size = state_machine.commit(0, .create_transfers, transfers_input, transfers_output);
1824
+ const errors = mem.bytesAsSlice(CreateTransfersResult, transfers_output[0..size]);
1825
+ try expectEqual(@as(usize, 0), errors.len);
1826
+ }
1219
1827
  for (transfers) |transfer| {
1220
- try testing.expectEqual(transfer, state_machine.get_transfer(transfer.id).?.*);
1828
+ try expectEqual(transfer, state_machine.get_transfer(transfer.id).?.*);
1221
1829
  }
1222
1830
 
1223
- // Commits:
1831
+ // Test balances before posting:
1832
+ try test_account_balances(&state_machine, 1, 52, 15, 0, 0);
1833
+ try test_account_balances(&state_machine, 2, 0, 0, 52, 15);
1834
+
1835
+ // Post pending transfers:
1836
+ const Vector = struct { result: CreateTransferResult, object: Transfer };
1224
1837
  const timestamp: u64 = (state_machine.commit_timestamp + 1);
1838
+
1225
1839
  const vectors = [_]Vector{
1226
- Vector{
1227
- .result = .reserved_field,
1228
- .object = std.mem.zeroInit(Commit, .{
1229
- .id = 1,
1840
+ .{
1841
+ .result = .ok,
1842
+ .object = mem.zeroInit(Transfer, .{
1843
+ .id = 101,
1844
+ .debit_account_id = 1,
1845
+ .credit_account_id = 2,
1846
+ .user_data = 1000,
1847
+ .pending_id = 2,
1848
+ .ledger = 1,
1849
+ .code = 1,
1850
+ .flags = .{ .post_pending_transfer = true },
1851
+ .amount = 13,
1230
1852
  .timestamp = timestamp,
1231
- .reserved = [_]u8{1} ** 32,
1232
1853
  }),
1233
1854
  },
1234
- Vector{
1235
- .result = .reserved_flag_padding,
1236
- .object = std.mem.zeroInit(Commit, .{
1237
- .id = 1,
1238
- .timestamp = timestamp,
1239
- .flags = .{ .padding = 1 },
1855
+ .{
1856
+ .result = .cannot_post_and_void_pending_transfer,
1857
+ .object = mem.zeroInit(Transfer, .{
1858
+ .id = 101,
1859
+ .debit_account_id = 10,
1860
+ .credit_account_id = 20,
1861
+ .user_data = 30,
1862
+ .pending_id = 0,
1863
+ .timeout = 50,
1864
+ .ledger = 60,
1865
+ .code = 70,
1866
+ .flags = .{
1867
+ .pending = true,
1868
+ .post_pending_transfer = true,
1869
+ .void_pending_transfer = true,
1870
+ },
1871
+ .amount = 80,
1872
+ .timestamp = timestamp + 1,
1240
1873
  }),
1241
1874
  },
1242
- Vector{
1243
- .result = .transfer_not_found,
1244
- .object = std.mem.zeroInit(Commit, .{
1245
- .id = 777,
1246
- .timestamp = timestamp,
1875
+ .{
1876
+ .result = .pending_transfer_cannot_post_or_void_another,
1877
+ .object = mem.zeroInit(Transfer, .{
1878
+ .id = 101,
1879
+ .debit_account_id = 10,
1880
+ .credit_account_id = 20,
1881
+ .user_data = 30,
1882
+ .pending_id = 0,
1883
+ .timeout = 50,
1884
+ .ledger = 60,
1885
+ .code = 70,
1886
+ .flags = .{
1887
+ .pending = true,
1888
+ .void_pending_transfer = true,
1889
+ },
1890
+ .amount = 80,
1891
+ .timestamp = timestamp + 1,
1247
1892
  }),
1248
1893
  },
1249
- Vector{
1250
- .result = .transfer_not_two_phase_commit,
1251
- .object = std.mem.zeroInit(Commit, .{
1252
- .id = 1,
1253
- .timestamp = timestamp,
1894
+ .{
1895
+ .result = .timeout_reserved_for_pending_transfer,
1896
+ .object = mem.zeroInit(Transfer, .{
1897
+ .id = 101,
1898
+ .debit_account_id = 10,
1899
+ .credit_account_id = 20,
1900
+ .user_data = 30,
1901
+ .timeout = 50,
1902
+ .ledger = 60,
1903
+ .code = 70,
1904
+ .flags = .{
1905
+ .void_pending_transfer = true,
1906
+ },
1907
+ .amount = 80,
1908
+ .timestamp = timestamp + 1,
1254
1909
  }),
1255
1910
  },
1256
- Vector{
1257
- .result = .ok,
1258
- .object = std.mem.zeroInit(Commit, .{
1259
- .id = 2,
1260
- .timestamp = timestamp,
1911
+ .{
1912
+ .result = .pending_id_must_not_be_zero,
1913
+ .object = mem.zeroInit(Transfer, .{
1914
+ .id = 101,
1915
+ .debit_account_id = 10,
1916
+ .credit_account_id = 20,
1917
+ .user_data = 30,
1918
+ .pending_id = 0,
1919
+ .ledger = 60,
1920
+ .code = 70,
1921
+ .flags = .{
1922
+ .void_pending_transfer = true,
1923
+ },
1924
+ .amount = 80,
1925
+ .timestamp = timestamp + 1,
1261
1926
  }),
1262
1927
  },
1263
- Vector{
1264
- .result = .already_committed_but_accepted,
1265
- .object = std.mem.zeroInit(Commit, .{
1266
- .id = 2,
1928
+ .{
1929
+ .result = .pending_id_must_be_different,
1930
+ .object = mem.zeroInit(Transfer, .{
1931
+ .id = 101,
1932
+ .debit_account_id = 10,
1933
+ .credit_account_id = 20,
1934
+ .user_data = 30,
1935
+ .pending_id = 101,
1936
+ .ledger = 60,
1937
+ .code = 70,
1938
+ .flags = .{
1939
+ .void_pending_transfer = true,
1940
+ },
1941
+ .amount = 80,
1267
1942
  .timestamp = timestamp + 1,
1268
- .flags = .{ .reject = true },
1269
1943
  }),
1270
1944
  },
1271
- Vector{
1272
- .result = .already_committed,
1273
- .object = std.mem.zeroInit(Commit, .{
1274
- .id = 2,
1945
+ .{
1946
+ .result = .pending_transfer_not_found,
1947
+ .object = mem.zeroInit(Transfer, .{
1948
+ .id = 101,
1949
+ .debit_account_id = 10,
1950
+ .credit_account_id = 20,
1951
+ .user_data = 30,
1952
+ .pending_id = 102,
1953
+ .ledger = 60,
1954
+ .code = 70,
1955
+ .flags = .{
1956
+ .void_pending_transfer = true,
1957
+ },
1958
+ .amount = 80,
1275
1959
  .timestamp = timestamp + 1,
1276
1960
  }),
1277
1961
  },
1278
- Vector{
1279
- .result = .ok,
1280
- .object = std.mem.zeroInit(Commit, .{
1281
- .id = 3,
1962
+ .{
1963
+ .result = .pending_transfer_not_pending,
1964
+ .object = mem.zeroInit(Transfer, .{
1965
+ .id = 101,
1966
+ .debit_account_id = 10,
1967
+ .credit_account_id = 20,
1968
+ .user_data = 30,
1969
+ .pending_id = 1,
1970
+ .ledger = 60,
1971
+ .code = 70,
1972
+ .flags = .{
1973
+ .void_pending_transfer = true,
1974
+ },
1975
+ .amount = 80,
1282
1976
  .timestamp = timestamp + 1,
1283
- .flags = .{ .reject = true },
1284
1977
  }),
1285
1978
  },
1286
- Vector{
1287
- .result = .already_committed_but_rejected,
1288
- .object = std.mem.zeroInit(Commit, .{
1289
- .id = 3,
1290
- .timestamp = timestamp + 2,
1979
+ .{
1980
+ .result = .pending_transfer_has_different_debit_account_id,
1981
+ .object = mem.zeroInit(Transfer, .{
1982
+ .id = 101,
1983
+ .debit_account_id = 10,
1984
+ .credit_account_id = 20,
1985
+ .user_data = 30,
1986
+ .pending_id = 2,
1987
+ .ledger = 60,
1988
+ .code = 70,
1989
+ .flags = .{
1990
+ .void_pending_transfer = true,
1991
+ },
1992
+ .amount = 80,
1993
+ .timestamp = timestamp + 1,
1291
1994
  }),
1292
1995
  },
1293
- Vector{
1294
- .result = .transfer_expired,
1295
- .object = std.mem.zeroInit(Commit, .{
1296
- .id = 4,
1297
- .timestamp = timestamp + 2,
1996
+ .{
1997
+ .result = .pending_transfer_has_different_credit_account_id,
1998
+ .object = mem.zeroInit(Transfer, .{
1999
+ .id = 101,
2000
+ .debit_account_id = 1,
2001
+ .credit_account_id = 20,
2002
+ .user_data = 30,
2003
+ .pending_id = 2,
2004
+ .ledger = 60,
2005
+ .code = 70,
2006
+ .flags = .{
2007
+ .void_pending_transfer = true,
2008
+ },
2009
+ .amount = 80,
2010
+ .timestamp = timestamp + 1,
2011
+ }),
2012
+ },
2013
+ .{
2014
+ .result = .pending_transfer_has_different_ledger,
2015
+ .object = mem.zeroInit(Transfer, .{
2016
+ .id = 101,
2017
+ .debit_account_id = 1,
2018
+ .credit_account_id = 2,
2019
+ .user_data = 30,
2020
+ .pending_id = 2,
2021
+ .ledger = 60,
2022
+ .code = 70,
2023
+ .flags = .{
2024
+ .void_pending_transfer = true,
2025
+ },
2026
+ .amount = 80,
2027
+ .timestamp = timestamp + 1,
2028
+ }),
2029
+ },
2030
+ .{
2031
+ .result = .pending_transfer_has_different_code,
2032
+ .object = mem.zeroInit(Transfer, .{
2033
+ .id = 101,
2034
+ .debit_account_id = 1,
2035
+ .credit_account_id = 2,
2036
+ .user_data = 30,
2037
+ .pending_id = 2,
2038
+ .ledger = 1,
2039
+ .code = 70,
2040
+ .flags = .{
2041
+ .void_pending_transfer = true,
2042
+ },
2043
+ .amount = 80,
2044
+ .timestamp = timestamp + 1,
2045
+ }),
2046
+ },
2047
+ .{
2048
+ .result = .exceeds_pending_transfer_amount,
2049
+ .object = mem.zeroInit(Transfer, .{
2050
+ .id = 101,
2051
+ .debit_account_id = 1,
2052
+ .credit_account_id = 2,
2053
+ .user_data = 7000,
2054
+ .pending_id = 2,
2055
+ .ledger = 1,
2056
+ .code = 1,
2057
+ .flags = .{
2058
+ .void_pending_transfer = true,
2059
+ },
2060
+ .amount = 80,
2061
+ .timestamp = timestamp + 1,
2062
+ }),
2063
+ },
2064
+ .{
2065
+ .result = .pending_transfer_has_different_amount,
2066
+ .object = mem.zeroInit(Transfer, .{
2067
+ .id = 101,
2068
+ .debit_account_id = 1,
2069
+ .credit_account_id = 2,
2070
+ .user_data = 7000,
2071
+ .pending_id = 2,
2072
+ .ledger = 1,
2073
+ .code = 1,
2074
+ .flags = .{
2075
+ .void_pending_transfer = true,
2076
+ },
2077
+ .amount = 1,
2078
+ .timestamp = timestamp + 1,
2079
+ }),
2080
+ },
2081
+ .{
2082
+ .result = .exists_with_different_flags,
2083
+ .object = mem.zeroInit(Transfer, .{
2084
+ .id = 101,
2085
+ .debit_account_id = 0,
2086
+ .credit_account_id = 0,
2087
+ .user_data = 7000,
2088
+ .pending_id = 3,
2089
+ .ledger = 0,
2090
+ .code = 0,
2091
+ .flags = .{
2092
+ .void_pending_transfer = true,
2093
+ },
2094
+ .amount = 15,
2095
+ .timestamp = timestamp + 1,
1298
2096
  }),
1299
2097
  },
1300
- Vector{
1301
- .result = .condition_requires_preimage,
1302
- .object = std.mem.zeroInit(Commit, .{
1303
- .id = 5,
2098
+ .{
2099
+ .result = .exists_with_different_pending_id,
2100
+ .object = mem.zeroInit(Transfer, .{
2101
+ .id = 101,
2102
+ .debit_account_id = 1,
2103
+ .credit_account_id = 2,
2104
+ .user_data = 7000,
2105
+ .pending_id = 3,
2106
+ .ledger = 1,
2107
+ .code = 1,
2108
+ .flags = .{
2109
+ .post_pending_transfer = true,
2110
+ },
2111
+ .amount = 14,
2112
+ .timestamp = timestamp + 1,
2113
+ }),
2114
+ },
2115
+ .{
2116
+ .result = .exists_with_different_user_data,
2117
+ .object = mem.zeroInit(Transfer, .{
2118
+ .id = 101,
2119
+ .debit_account_id = 1,
2120
+ .credit_account_id = 2,
2121
+ .user_data = 7000,
2122
+ .pending_id = 2,
2123
+ .ledger = 1,
2124
+ .code = 1,
2125
+ .flags = .{
2126
+ .post_pending_transfer = true,
2127
+ },
2128
+ .amount = 14,
2129
+ .timestamp = timestamp + 1,
2130
+ }),
2131
+ },
2132
+ .{
2133
+ .result = .exists_with_different_user_data,
2134
+ .object = mem.zeroInit(Transfer, .{
2135
+ .id = 101,
2136
+ .debit_account_id = 1,
2137
+ .credit_account_id = 2,
2138
+ .user_data = 0,
2139
+ .pending_id = 2,
2140
+ .ledger = 1,
2141
+ .code = 1,
2142
+ .flags = .{
2143
+ .post_pending_transfer = true,
2144
+ },
2145
+ .amount = 14,
2146
+ .timestamp = timestamp + 1,
2147
+ }),
2148
+ },
2149
+ .{
2150
+ .result = .exists_with_different_amount,
2151
+ .object = mem.zeroInit(Transfer, .{
2152
+ .id = 101,
2153
+ .debit_account_id = 1,
2154
+ .credit_account_id = 2,
2155
+ .user_data = 1000,
2156
+ .pending_id = 2,
2157
+ .ledger = 1,
2158
+ .code = 1,
2159
+ .flags = .{
2160
+ .post_pending_transfer = true,
2161
+ },
2162
+ .amount = 14,
2163
+ .timestamp = timestamp + 1,
2164
+ }),
2165
+ },
2166
+ .{
2167
+ .result = .exists_with_different_amount,
2168
+ .object = mem.zeroInit(Transfer, .{
2169
+ .id = 101,
2170
+ .debit_account_id = 1,
2171
+ .credit_account_id = 2,
2172
+ .user_data = 1000,
2173
+ .pending_id = 2,
2174
+ .ledger = 1,
2175
+ .code = 1,
2176
+ .flags = .{
2177
+ .post_pending_transfer = true,
2178
+ },
2179
+ .amount = 0,
2180
+ .timestamp = timestamp + 1,
2181
+ }),
2182
+ },
2183
+ .{
2184
+ .result = .exists,
2185
+ .object = mem.zeroInit(Transfer, .{
2186
+ .id = 101,
2187
+ .debit_account_id = 1,
2188
+ .credit_account_id = 2,
2189
+ .user_data = 1000,
2190
+ .pending_id = 2,
2191
+ .ledger = 1,
2192
+ .code = 1,
2193
+ .flags = .{
2194
+ .post_pending_transfer = true,
2195
+ },
2196
+ .amount = 13,
2197
+ .timestamp = timestamp + 1,
2198
+ }),
2199
+ },
2200
+ .{
2201
+ .result = .pending_transfer_already_posted,
2202
+ .object = mem.zeroInit(Transfer, .{
2203
+ .id = 102,
2204
+ .debit_account_id = 1,
2205
+ .credit_account_id = 2,
2206
+ .user_data = 1000,
2207
+ .pending_id = 2,
2208
+ .ledger = 1,
2209
+ .code = 1,
2210
+ .flags = .{
2211
+ .post_pending_transfer = true,
2212
+ },
2213
+ .amount = 13,
2214
+ .timestamp = timestamp + 1,
2215
+ }),
2216
+ },
2217
+ .{
2218
+ .result = .ok,
2219
+ .object = mem.zeroInit(Transfer, .{
2220
+ .id = 103,
2221
+ .debit_account_id = 0,
2222
+ .credit_account_id = 0,
2223
+ .user_data = 0,
2224
+ .pending_id = 3,
2225
+ .ledger = 0,
2226
+ .code = 0,
2227
+ .flags = .{ .void_pending_transfer = true },
2228
+ .amount = 15,
2229
+ .timestamp = timestamp + 1,
2230
+ }),
2231
+ },
2232
+ .{
2233
+ .result = .pending_transfer_already_voided,
2234
+ .object = mem.zeroInit(Transfer, .{
2235
+ .id = 102,
2236
+ .debit_account_id = 1,
2237
+ .credit_account_id = 2,
2238
+ .user_data = 1000,
2239
+ .pending_id = 3,
2240
+ .ledger = 1,
2241
+ .code = 1,
2242
+ .flags = .{
2243
+ .post_pending_transfer = true,
2244
+ },
2245
+ .amount = 13,
1304
2246
  .timestamp = timestamp + 2,
1305
2247
  }),
1306
2248
  },
1307
- Vector{
1308
- .result = .preimage_invalid,
1309
- .object = std.mem.zeroInit(Commit, .{
1310
- .id = 5,
2249
+ .{
2250
+ .result = .pending_transfer_expired,
2251
+ .object = mem.zeroInit(Transfer, .{
2252
+ .id = 102,
2253
+ .debit_account_id = 1,
2254
+ .credit_account_id = 2,
2255
+ .user_data = 4000,
2256
+ .pending_id = 4,
2257
+ .ledger = 1,
2258
+ .code = 1,
2259
+ .flags = .{
2260
+ .void_pending_transfer = true,
2261
+ },
2262
+ .amount = 15,
1311
2263
  .timestamp = timestamp + 2,
1312
- .flags = .{ .preimage = true },
1313
- .reserved = [_]u8{1} ** 32,
1314
2264
  }),
1315
2265
  },
1316
- Vector{
1317
- .result = .preimage_requires_condition,
1318
- .object = std.mem.zeroInit(Commit, .{
1319
- .id = 6,
2266
+ .{
2267
+ .result = .ok,
2268
+ .object = mem.zeroInit(Transfer, .{
2269
+ .id = 105,
2270
+ .debit_account_id = 0,
2271
+ .credit_account_id = 0,
2272
+ .user_data = 0,
2273
+ .pending_id = 5,
2274
+ .ledger = 0,
2275
+ .code = 0,
2276
+ .flags = .{ .post_pending_transfer = true },
2277
+ .amount = 0,
1320
2278
  .timestamp = timestamp + 2,
1321
- .flags = .{ .preimage = true },
1322
2279
  }),
1323
2280
  },
1324
2281
  };
1325
2282
 
1326
- // Test balances BEFORE commit
1327
- // Account 1:
1328
- const account_1_before = state_machine.get_account(1).?.*;
1329
- try testing.expectEqual(@as(u64, 15), account_1_before.debits_accepted);
1330
- try testing.expectEqual(@as(u64, 75), account_1_before.debits_reserved);
1331
- try testing.expectEqual(@as(u64, 0), account_1_before.credits_accepted);
1332
- try testing.expectEqual(@as(u64, 0), account_1_before.credits_reserved);
1333
- // Account 2:
1334
- const account_2_before = state_machine.get_account(2).?.*;
1335
- try testing.expectEqual(@as(u64, 0), account_2_before.debits_accepted);
1336
- try testing.expectEqual(@as(u64, 0), account_2_before.debits_reserved);
1337
- try testing.expectEqual(@as(u64, 15), account_2_before.credits_accepted);
1338
- try testing.expectEqual(@as(u64, 75), account_2_before.credits_reserved);
1339
-
1340
- for (vectors) |vector| {
1341
- try testing.expectEqual(vector.result, state_machine.commit_transfer(vector.object));
2283
+ for (vectors) |vector, i| {
2284
+ const result = state_machine.create_transfer(&vector.object);
2285
+ expectEqual(vector.result, result) catch |err| {
2286
+ print_test_vector(i, vector.result, result, vector.object, err);
2287
+ return err;
2288
+ };
2289
+
1342
2290
  if (vector.result == .ok) {
1343
- try testing.expectEqual(vector.object, state_machine.get_commit(vector.object.id).?.*);
2291
+ // Test that posted values inherit from the pending or posting transfer:
2292
+ const pending = state_machine.get_transfer(vector.object.pending_id).?.*;
2293
+ const posted = state_machine.get_transfer(vector.object.id).?.*;
2294
+
2295
+ const user_data = if (vector.object.user_data == 0)
2296
+ pending.user_data
2297
+ else
2298
+ vector.object.user_data;
2299
+
2300
+ const amount = if (vector.object.amount == 0)
2301
+ pending.amount
2302
+ else
2303
+ vector.object.amount;
2304
+
2305
+ const expected = Transfer{
2306
+ .id = vector.object.id,
2307
+ .debit_account_id = pending.debit_account_id,
2308
+ .credit_account_id = pending.credit_account_id,
2309
+ .user_data = user_data,
2310
+ .reserved = 0,
2311
+ .pending_id = pending.id,
2312
+ .timeout = 0,
2313
+ .ledger = pending.ledger,
2314
+ .code = pending.code,
2315
+ .flags = vector.object.flags,
2316
+ .amount = amount,
2317
+ .timestamp = vector.object.timestamp,
2318
+ };
2319
+ expectEqual(expected, posted) catch |err| {
2320
+ print_test_vector(
2321
+ i,
2322
+ expected,
2323
+ posted,
2324
+ vector.object,
2325
+ err,
2326
+ );
2327
+ return err;
2328
+ };
1344
2329
  }
1345
2330
  }
1346
2331
 
1347
- // Test balances AFTER commit
1348
- // Account 1:
1349
- const account_1_after = state_machine.get_account(1).?.*;
1350
- try testing.expectEqual(@as(u64, 30), account_1_after.debits_accepted);
1351
- // +15 (acceptance applied):
1352
- try testing.expectEqual(@as(u64, 45), account_1_after.debits_reserved);
1353
- // -15 (reserved moved):
1354
- try testing.expectEqual(@as(u64, 0), account_1_after.credits_accepted);
1355
- try testing.expectEqual(@as(u64, 0), account_1_after.credits_reserved);
1356
- // Account 2:
1357
- const account_2_after = state_machine.get_account(2).?.*;
1358
- try testing.expectEqual(@as(u64, 0), account_2_after.debits_accepted);
1359
- try testing.expectEqual(@as(u64, 0), account_2_after.debits_reserved);
1360
- // +15 (acceptance applied):
1361
- try testing.expectEqual(@as(u64, 30), account_2_after.credits_accepted);
1362
- // -15 (reserved moved):
1363
- try testing.expectEqual(@as(u64, 45), account_2_after.credits_reserved);
1364
-
1365
- // Test COMMIT with invalid debit/credit accounts
1366
- state_machine.create_account_rollback(accounts[3]);
1367
- try testing.expect(state_machine.get_account(accounts[3].id) == null);
1368
- try testing.expectEqual(
1369
- state_machine.commit_transfer(std.mem.zeroInit(Commit, .{
1370
- .id = 7,
1371
- .timestamp = timestamp + 2,
1372
- })),
1373
- .credit_account_not_found,
1374
- );
1375
- state_machine.create_account_rollback(accounts[2]);
1376
- try testing.expect(state_machine.get_account(accounts[2].id) == null);
1377
- try testing.expectEqual(
1378
- state_machine.commit_transfer(std.mem.zeroInit(Commit, .{
1379
- .id = 7,
1380
- .timestamp = timestamp + 2,
1381
- })),
1382
- .debit_account_not_found,
1383
- );
2332
+ // Balances after posting:
2333
+ try test_account_balances(&state_machine, 1, 15, 35, 0, 0);
2334
+ try test_account_balances(&state_machine, 2, 0, 0, 15, 35);
2335
+
2336
+ // Rollback posting transfer (different amount):
2337
+ assert(vectors[0].result == .ok);
2338
+ try test_transfer_rollback(&state_machine, &vectors[0].object);
2339
+ try test_account_balances(&state_machine, 1, 30, 22, 0, 0);
2340
+ try test_account_balances(&state_machine, 2, 0, 0, 30, 22);
2341
+
2342
+ // Rollback voiding transfer:
2343
+ assert(vectors[22].result == .ok);
2344
+ try test_transfer_rollback(&state_machine, &vectors[22].object);
2345
+ try test_account_balances(&state_machine, 1, 45, 22, 0, 0);
2346
+ try test_account_balances(&state_machine, 2, 0, 0, 45, 22);
2347
+
2348
+ // Rollback posting transfer (zero amount):
2349
+ assert(vectors[25].result == .ok);
2350
+ try test_transfer_rollback(&state_machine, &vectors[25].object);
2351
+ try test_account_balances(&state_machine, 1, 52, 15, 0, 0);
2352
+ try test_account_balances(&state_machine, 2, 0, 0, 52, 15);
2353
+
2354
+ // Rollback all pending transfers:
2355
+ try test_transfer_rollback(&state_machine, &transfers[1]);
2356
+ try test_account_balances(&state_machine, 1, 37, 15, 0, 0);
2357
+ try test_account_balances(&state_machine, 2, 0, 0, 37, 15);
2358
+
2359
+ try test_transfer_rollback(&state_machine, &transfers[2]);
2360
+ try test_account_balances(&state_machine, 1, 22, 15, 0, 0);
2361
+ try test_account_balances(&state_machine, 2, 0, 0, 22, 15);
2362
+
2363
+ try test_transfer_rollback(&state_machine, &transfers[3]);
2364
+ try test_account_balances(&state_machine, 1, 7, 15, 0, 0);
2365
+ try test_account_balances(&state_machine, 2, 0, 0, 7, 15);
2366
+
2367
+ try test_transfer_rollback(&state_machine, &transfers[4]);
2368
+ try test_account_balances(&state_machine, 1, 0, 15, 0, 0);
2369
+ try test_account_balances(&state_machine, 2, 0, 0, 0, 15);
2370
+
2371
+ // Rollback transfer:
2372
+ try test_transfer_rollback(&state_machine, &transfers[0]);
2373
+ try test_account_balances(&state_machine, 1, 0, 0, 0, 0);
2374
+ try test_account_balances(&state_machine, 2, 0, 0, 0, 0);
2375
+ }
1384
2376
 
1385
- // Rollback [id=2] not rejected:
1386
- state_machine.commit_transfer_rollback(vectors[4].object);
1387
-
1388
- // Account 1:
1389
- const account_1_rollback = state_machine.get_account(1).?.*;
1390
- // -15 (rollback):
1391
- try testing.expectEqual(@as(u64, 15), account_1_rollback.debits_accepted);
1392
- try testing.expectEqual(@as(u64, 60), account_1_rollback.debits_reserved);
1393
- try testing.expectEqual(@as(u64, 0), account_1_rollback.credits_accepted);
1394
- try testing.expectEqual(@as(u64, 0), account_1_rollback.credits_reserved);
1395
- // Account 2:
1396
- const account_2_rollback = state_machine.get_account(2).?.*;
1397
- try testing.expectEqual(@as(u64, 0), account_2_rollback.debits_accepted);
1398
- try testing.expectEqual(@as(u64, 0), account_2_rollback.debits_reserved);
1399
- // -15 (rollback):
1400
- try testing.expectEqual(@as(u64, 15), account_2_rollback.credits_accepted);
1401
- try testing.expectEqual(@as(u64, 60), account_2_rollback.credits_reserved);
1402
-
1403
- // Rollback [id=3] rejected:
1404
- state_machine.commit_transfer_rollback(vectors[7].object);
1405
- // Account 1:
1406
- const account_1_rollback_reject = state_machine.get_account(1).?.*;
1407
- try testing.expectEqual(@as(u64, 15), account_1_rollback_reject.debits_accepted);
1408
- // Remains unchanged:
1409
- try testing.expectEqual(@as(u64, 75), account_1_rollback_reject.debits_reserved);
1410
- // +15 rolled back:
1411
- try testing.expectEqual(@as(u64, 0), account_1_rollback_reject.credits_accepted);
1412
- try testing.expectEqual(@as(u64, 0), account_1_rollback_reject.credits_reserved);
1413
- // Account 2:
1414
- const account_2_rollback_reject = state_machine.get_account(2).?.*;
1415
- try testing.expectEqual(@as(u64, 0), account_2_rollback_reject.debits_accepted);
1416
- try testing.expectEqual(@as(u64, 0), account_2_rollback_reject.debits_reserved);
1417
- try testing.expectEqual(@as(u64, 15), account_2_rollback_reject.credits_accepted);
1418
- // +15 rolled back"
1419
- try testing.expectEqual(@as(u64, 75), account_2_rollback_reject.credits_reserved);
2377
+ fn print_test_vector(
2378
+ i: usize,
2379
+ result_expect: anytype,
2380
+ result_actual: anytype,
2381
+ vector_object: anytype,
2382
+ err: anyerror,
2383
+ ) void {
2384
+ std.debug.print("\nindex={}\n\nexpect: {}\n\nactual: {}\n\nobject: {}\n\nerr={}", .{
2385
+ i,
2386
+ result_expect,
2387
+ result_actual,
2388
+ vector_object,
2389
+ err,
2390
+ });
1420
2391
  }
1421
2392
 
1422
- fn test_routine_zeroed(comptime len: usize) !void {
1423
- const routine = switch (len) {
2393
+ fn test_account_balances(
2394
+ state_machine: *const StateMachine,
2395
+ account_id: u128,
2396
+ debits_pending: u64,
2397
+ debits_posted: u64,
2398
+ credits_pending: u64,
2399
+ credits_posted: u64,
2400
+ ) !void {
2401
+ const account = state_machine.get_account(account_id).?.*;
2402
+ try expectEqual(debits_pending, account.debits_pending);
2403
+ try expectEqual(debits_posted, account.debits_posted);
2404
+ try expectEqual(credits_pending, account.credits_pending);
2405
+ try expectEqual(credits_posted, account.credits_posted);
2406
+ }
2407
+
2408
+ fn test_transfer_rollback(state_machine: *StateMachine, transfer: *const Transfer) !void {
2409
+ assert(state_machine.get_transfer(transfer.id) != null);
2410
+
2411
+ state_machine.create_transfer_rollback(transfer);
2412
+
2413
+ try expect(state_machine.get_transfer(transfer.id) == null);
2414
+ if (transfer.flags.post_pending_transfer or transfer.flags.void_pending_transfer) {
2415
+ try expect(state_machine.get_posted(transfer.pending_id) == null);
2416
+ }
2417
+ }
2418
+
2419
+ test "zeroed_32_bytes" {
2420
+ try test_zeroed_n_bytes(32);
2421
+ }
2422
+
2423
+ test "zeroed_48_bytes" {
2424
+ try test_zeroed_n_bytes(48);
2425
+ }
2426
+
2427
+ fn test_zeroed_n_bytes(comptime n: usize) !void {
2428
+ const routine = switch (n) {
1424
2429
  32 => zeroed_32_bytes,
1425
2430
  48 => zeroed_48_bytes,
1426
2431
  else => unreachable,
1427
2432
  };
1428
- var a = [_]u8{0} ** len;
2433
+ var a = [_]u8{0} ** n;
1429
2434
  var i: usize = 0;
1430
2435
  while (i < a.len) : (i += 1) {
1431
2436
  a[i] = 1;
1432
- try testing.expectEqual(false, routine(a));
2437
+ try expectEqual(false, routine(a));
1433
2438
  a[i] = 0;
1434
2439
  }
1435
- try testing.expectEqual(true, routine(a));
2440
+ try expectEqual(true, routine(a));
2441
+ }
2442
+
2443
+ test "equal_32_bytes" {
2444
+ try test_equal_n_bytes(32);
2445
+ }
2446
+
2447
+ test "equal_48_bytes" {
2448
+ try test_equal_n_bytes(48);
1436
2449
  }
1437
2450
 
1438
- fn test_routine_equal(comptime len: usize) !void {
1439
- const routine = switch (len) {
2451
+ fn test_equal_n_bytes(comptime n: usize) !void {
2452
+ const routine = switch (n) {
1440
2453
  32 => equal_32_bytes,
1441
2454
  48 => equal_48_bytes,
1442
2455
  else => unreachable,
1443
2456
  };
1444
- var a = [_]u8{0} ** len;
1445
- var b = [_]u8{0} ** len;
2457
+ var a = [_]u8{0} ** n;
2458
+ var b = [_]u8{0} ** n;
1446
2459
  var i: usize = 0;
1447
2460
  while (i < a.len) : (i += 1) {
1448
2461
  a[i] = 1;
1449
- try testing.expectEqual(false, routine(a, b));
2462
+ try expectEqual(false, routine(a, b));
1450
2463
  a[i] = 0;
1451
2464
 
1452
2465
  b[i] = 1;
1453
- try testing.expectEqual(false, routine(a, b));
2466
+ try expectEqual(false, routine(a, b));
1454
2467
  b[i] = 0;
1455
2468
  }
1456
- try testing.expectEqual(true, routine(a, b));
1457
- }
1458
-
1459
- test "zeroed_32_bytes" {
1460
- try test_routine_zeroed(32);
1461
- }
1462
-
1463
- test "zeroed_48_bytes" {
1464
- try test_routine_zeroed(48);
1465
- }
1466
-
1467
- test "equal_32_bytes" {
1468
- try test_routine_equal(32);
1469
- }
1470
-
1471
- test "equal_48_bytes" {
1472
- try test_routine_equal(48);
2469
+ try expectEqual(true, routine(a, b));
1473
2470
  }