tigerbeetle-node 0.11.12 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +212 -196
- package/dist/bin/aarch64-linux-gnu/client.node +0 -0
- package/dist/bin/aarch64-linux-musl/client.node +0 -0
- package/dist/bin/aarch64-macos/client.node +0 -0
- package/dist/bin/x86_64-linux-gnu/client.node +0 -0
- package/dist/bin/x86_64-linux-musl/client.node +0 -0
- package/dist/bin/x86_64-macos/client.node +0 -0
- package/dist/index.js +33 -1
- package/dist/index.js.map +1 -1
- package/package-lock.json +66 -0
- package/package.json +8 -17
- package/src/index.ts +56 -1
- package/src/node.zig +10 -9
- package/dist/.client.node.sha256 +0 -1
- package/scripts/build_lib.sh +0 -61
- package/scripts/download_node_headers.sh +0 -32
- package/src/tigerbeetle/scripts/benchmark.bat +0 -48
- package/src/tigerbeetle/scripts/benchmark.sh +0 -66
- package/src/tigerbeetle/scripts/confirm_image.sh +0 -44
- package/src/tigerbeetle/scripts/fuzz_loop.sh +0 -15
- package/src/tigerbeetle/scripts/fuzz_unique_errors.sh +0 -7
- package/src/tigerbeetle/scripts/install.bat +0 -7
- package/src/tigerbeetle/scripts/install.sh +0 -21
- package/src/tigerbeetle/scripts/install_zig.bat +0 -113
- package/src/tigerbeetle/scripts/install_zig.sh +0 -90
- package/src/tigerbeetle/scripts/lint.zig +0 -199
- package/src/tigerbeetle/scripts/pre-commit.sh +0 -9
- package/src/tigerbeetle/scripts/scripts/benchmark.bat +0 -48
- package/src/tigerbeetle/scripts/scripts/benchmark.sh +0 -66
- package/src/tigerbeetle/scripts/scripts/confirm_image.sh +0 -44
- package/src/tigerbeetle/scripts/scripts/fuzz_loop.sh +0 -15
- package/src/tigerbeetle/scripts/scripts/fuzz_unique_errors.sh +0 -7
- package/src/tigerbeetle/scripts/scripts/install.bat +0 -7
- package/src/tigerbeetle/scripts/scripts/install.sh +0 -21
- package/src/tigerbeetle/scripts/scripts/install_zig.bat +0 -113
- package/src/tigerbeetle/scripts/scripts/install_zig.sh +0 -90
- package/src/tigerbeetle/scripts/scripts/lint.zig +0 -199
- package/src/tigerbeetle/scripts/scripts/pre-commit.sh +0 -9
- package/src/tigerbeetle/scripts/scripts/shellcheck.sh +0 -5
- package/src/tigerbeetle/scripts/scripts/tests_on_alpine.sh +0 -10
- package/src/tigerbeetle/scripts/scripts/tests_on_ubuntu.sh +0 -14
- package/src/tigerbeetle/scripts/scripts/upgrade_ubuntu_kernel.sh +0 -48
- package/src/tigerbeetle/scripts/scripts/validate_docs.sh +0 -23
- package/src/tigerbeetle/scripts/scripts/vr_state_enumerate +0 -46
- package/src/tigerbeetle/scripts/shellcheck.sh +0 -5
- package/src/tigerbeetle/scripts/tests_on_alpine.sh +0 -10
- package/src/tigerbeetle/scripts/tests_on_ubuntu.sh +0 -14
- package/src/tigerbeetle/scripts/upgrade_ubuntu_kernel.sh +0 -48
- package/src/tigerbeetle/scripts/validate_docs.sh +0 -23
- package/src/tigerbeetle/scripts/vr_state_enumerate +0 -46
- package/src/tigerbeetle/src/benchmark.zig +0 -314
- package/src/tigerbeetle/src/config.zig +0 -234
- package/src/tigerbeetle/src/constants.zig +0 -436
- package/src/tigerbeetle/src/ewah.zig +0 -286
- package/src/tigerbeetle/src/ewah_benchmark.zig +0 -120
- package/src/tigerbeetle/src/ewah_fuzz.zig +0 -130
- package/src/tigerbeetle/src/fifo.zig +0 -120
- package/src/tigerbeetle/src/io/benchmark.zig +0 -213
- package/src/tigerbeetle/src/io/darwin.zig +0 -814
- package/src/tigerbeetle/src/io/linux.zig +0 -1062
- package/src/tigerbeetle/src/io/test.zig +0 -643
- package/src/tigerbeetle/src/io/windows.zig +0 -1183
- package/src/tigerbeetle/src/io.zig +0 -34
- package/src/tigerbeetle/src/iops.zig +0 -107
- package/src/tigerbeetle/src/lsm/README.md +0 -308
- package/src/tigerbeetle/src/lsm/binary_search.zig +0 -341
- package/src/tigerbeetle/src/lsm/bloom_filter.zig +0 -125
- package/src/tigerbeetle/src/lsm/compaction.zig +0 -603
- package/src/tigerbeetle/src/lsm/composite_key.zig +0 -77
- package/src/tigerbeetle/src/lsm/direction.zig +0 -11
- package/src/tigerbeetle/src/lsm/eytzinger.zig +0 -587
- package/src/tigerbeetle/src/lsm/eytzinger_benchmark.zig +0 -330
- package/src/tigerbeetle/src/lsm/forest.zig +0 -204
- package/src/tigerbeetle/src/lsm/forest_fuzz.zig +0 -401
- package/src/tigerbeetle/src/lsm/grid.zig +0 -573
- package/src/tigerbeetle/src/lsm/groove.zig +0 -972
- package/src/tigerbeetle/src/lsm/k_way_merge.zig +0 -474
- package/src/tigerbeetle/src/lsm/level_iterator.zig +0 -332
- package/src/tigerbeetle/src/lsm/manifest.zig +0 -617
- package/src/tigerbeetle/src/lsm/manifest_level.zig +0 -877
- package/src/tigerbeetle/src/lsm/manifest_log.zig +0 -789
- package/src/tigerbeetle/src/lsm/manifest_log_fuzz.zig +0 -691
- package/src/tigerbeetle/src/lsm/merge_iterator.zig +0 -106
- package/src/tigerbeetle/src/lsm/node_pool.zig +0 -235
- package/src/tigerbeetle/src/lsm/posted_groove.zig +0 -378
- package/src/tigerbeetle/src/lsm/segmented_array.zig +0 -1328
- package/src/tigerbeetle/src/lsm/segmented_array_benchmark.zig +0 -148
- package/src/tigerbeetle/src/lsm/segmented_array_fuzz.zig +0 -9
- package/src/tigerbeetle/src/lsm/set_associative_cache.zig +0 -850
- package/src/tigerbeetle/src/lsm/table.zig +0 -1031
- package/src/tigerbeetle/src/lsm/table_immutable.zig +0 -203
- package/src/tigerbeetle/src/lsm/table_iterator.zig +0 -340
- package/src/tigerbeetle/src/lsm/table_mutable.zig +0 -220
- package/src/tigerbeetle/src/lsm/test.zig +0 -438
- package/src/tigerbeetle/src/lsm/tree.zig +0 -1193
- package/src/tigerbeetle/src/lsm/tree_fuzz.zig +0 -474
- package/src/tigerbeetle/src/message_bus.zig +0 -1012
- package/src/tigerbeetle/src/message_pool.zig +0 -156
- package/src/tigerbeetle/src/ring_buffer.zig +0 -399
- package/src/tigerbeetle/src/simulator.zig +0 -569
- package/src/tigerbeetle/src/state_machine/auditor.zig +0 -577
- package/src/tigerbeetle/src/state_machine/workload.zig +0 -883
- package/src/tigerbeetle/src/state_machine.zig +0 -1881
- package/src/tigerbeetle/src/static_allocator.zig +0 -65
- package/src/tigerbeetle/src/stdx.zig +0 -162
- package/src/tigerbeetle/src/storage.zig +0 -393
- package/src/tigerbeetle/src/testing/cluster/message_bus.zig +0 -82
- package/src/tigerbeetle/src/testing/cluster/network.zig +0 -237
- package/src/tigerbeetle/src/testing/cluster/state_checker.zig +0 -169
- package/src/tigerbeetle/src/testing/cluster/storage_checker.zig +0 -202
- package/src/tigerbeetle/src/testing/cluster.zig +0 -443
- package/src/tigerbeetle/src/testing/fuzz.zig +0 -140
- package/src/tigerbeetle/src/testing/hash_log.zig +0 -66
- package/src/tigerbeetle/src/testing/id.zig +0 -99
- package/src/tigerbeetle/src/testing/packet_simulator.zig +0 -364
- package/src/tigerbeetle/src/testing/priority_queue.zig +0 -645
- package/src/tigerbeetle/src/testing/reply_sequence.zig +0 -139
- package/src/tigerbeetle/src/testing/state_machine.zig +0 -249
- package/src/tigerbeetle/src/testing/storage.zig +0 -757
- package/src/tigerbeetle/src/testing/table.zig +0 -247
- package/src/tigerbeetle/src/testing/time.zig +0 -84
- package/src/tigerbeetle/src/tigerbeetle.zig +0 -227
- package/src/tigerbeetle/src/time.zig +0 -112
- package/src/tigerbeetle/src/tracer.zig +0 -529
- package/src/tigerbeetle/src/unit_tests.zig +0 -42
- package/src/tigerbeetle/src/vopr.zig +0 -495
- package/src/tigerbeetle/src/vsr/README.md +0 -209
- package/src/tigerbeetle/src/vsr/client.zig +0 -544
- package/src/tigerbeetle/src/vsr/clock.zig +0 -853
- package/src/tigerbeetle/src/vsr/journal.zig +0 -2413
- package/src/tigerbeetle/src/vsr/journal_format_fuzz.zig +0 -111
- package/src/tigerbeetle/src/vsr/marzullo.zig +0 -309
- package/src/tigerbeetle/src/vsr/replica.zig +0 -6381
- package/src/tigerbeetle/src/vsr/replica_format.zig +0 -219
- package/src/tigerbeetle/src/vsr/superblock.zig +0 -1631
- package/src/tigerbeetle/src/vsr/superblock_client_table.zig +0 -256
- package/src/tigerbeetle/src/vsr/superblock_free_set.zig +0 -929
- package/src/tigerbeetle/src/vsr/superblock_free_set_fuzz.zig +0 -334
- package/src/tigerbeetle/src/vsr/superblock_fuzz.zig +0 -390
- package/src/tigerbeetle/src/vsr/superblock_manifest.zig +0 -615
- package/src/tigerbeetle/src/vsr/superblock_quorums.zig +0 -394
- package/src/tigerbeetle/src/vsr/superblock_quorums_fuzz.zig +0 -314
- package/src/tigerbeetle/src/vsr.zig +0 -1352
|
@@ -1,883 +0,0 @@
|
|
|
1
|
-
//! The Workload drives an end-to-end test: from client requests, through consensus and the state
|
|
2
|
-
//! machine, down to the storage engine, and back.
|
|
3
|
-
//!
|
|
4
|
-
//! The Workload constructs messages to create and query accounts and transfers, and validates the
|
|
5
|
-
//! replies.
|
|
6
|
-
//!
|
|
7
|
-
//! Goals:
|
|
8
|
-
//!
|
|
9
|
-
//! * Run in a fixed amount of memory. (For long-running tests or performance testing).
|
|
10
|
-
//! * Query and verify transfers arbitrarily far back. (To exercise the storage engine).
|
|
11
|
-
//!
|
|
12
|
-
//! Transfer Encoding:
|
|
13
|
-
//!
|
|
14
|
-
//! * `Transfer.id` is a deterministic, reversible permutation of an ascending index.
|
|
15
|
-
//! * With the transfer's index as a seed, the Workload knows the eventual outcome of the transfer.
|
|
16
|
-
//! * `Transfer.user_data` is a checksum of the remainder of the transfer's data
|
|
17
|
-
//! (excluding `timestamp` and `user_data` itself). This helps `on_lookup_transfers` to
|
|
18
|
-
//! validate its results.
|
|
19
|
-
//!
|
|
20
|
-
const std = @import("std");
|
|
21
|
-
const assert = std.debug.assert;
|
|
22
|
-
const log = std.log.scoped(.test_workload);
|
|
23
|
-
|
|
24
|
-
const constants = @import("../constants.zig");
|
|
25
|
-
const tb = @import("../tigerbeetle.zig");
|
|
26
|
-
const vsr = @import("../vsr.zig");
|
|
27
|
-
const accounting_auditor = @import("auditor.zig");
|
|
28
|
-
const Auditor = accounting_auditor.AccountingAuditor;
|
|
29
|
-
const IdPermutation = @import("../testing/id.zig").IdPermutation;
|
|
30
|
-
const fuzz = @import("../testing/fuzz.zig");
|
|
31
|
-
|
|
32
|
-
// TODO(zig) This won't be necessary in Zig 0.10.
|
|
33
|
-
const PriorityQueue = @import("../testing/priority_queue.zig").PriorityQueue;
|
|
34
|
-
|
|
35
|
-
const TransferOutcome = enum {
|
|
36
|
-
/// The transfer is guaranteed to commit.
|
|
37
|
-
/// For example, a single-phase transfer between valid accounts without balance limits.
|
|
38
|
-
success,
|
|
39
|
-
/// The transfer is invalid. For example, the `ledger` field is missing.
|
|
40
|
-
failure,
|
|
41
|
-
/// Due to races with timeouts or other transfers, the outcome of the transfer is uncertain.
|
|
42
|
-
/// For example, post/void-pending transfers race with their timeout.
|
|
43
|
-
unknown,
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
/// A Transfer generated from the plan is guaranteed to have a matching `outcome`, but it may use a
|
|
47
|
-
/// different Method. (For example, `method=pending` may fall back to `method=single_phase` if the
|
|
48
|
-
/// Auditor's pending transfer queue is full).
|
|
49
|
-
const TransferPlan = struct {
|
|
50
|
-
/// When false, send invalid payments that are guaranteed to be rejected with an error.
|
|
51
|
-
valid: bool,
|
|
52
|
-
|
|
53
|
-
/// When `limit` is set, at least one of the following is true:
|
|
54
|
-
///
|
|
55
|
-
/// * the debit account has debits_must_not_exceed_credits
|
|
56
|
-
/// * the credit account has credits_must_not_exceed_debits
|
|
57
|
-
///
|
|
58
|
-
limit: bool,
|
|
59
|
-
|
|
60
|
-
method: Method,
|
|
61
|
-
|
|
62
|
-
const Method = enum {
|
|
63
|
-
single_phase,
|
|
64
|
-
pending,
|
|
65
|
-
post_pending,
|
|
66
|
-
void_pending,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
fn outcome(self: TransferPlan) TransferOutcome {
|
|
70
|
-
if (!self.valid) return .failure;
|
|
71
|
-
if (self.limit) return .unknown;
|
|
72
|
-
return switch (self.method) {
|
|
73
|
-
.single_phase, .pending => .success,
|
|
74
|
-
.post_pending, .void_pending => .unknown,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const TransferTemplate = struct {
|
|
80
|
-
ledger: u32,
|
|
81
|
-
result: accounting_auditor.CreateTransferResultSet,
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const TransferBatchQueue = PriorityQueue(TransferBatch, void, struct {
|
|
85
|
-
/// Ascending order.
|
|
86
|
-
fn compare(_: void, a: TransferBatch, b: TransferBatch) std.math.Order {
|
|
87
|
-
assert(a.min != b.min);
|
|
88
|
-
assert(a.max != b.max);
|
|
89
|
-
return std.math.order(a.min, b.min);
|
|
90
|
-
}
|
|
91
|
-
}.compare);
|
|
92
|
-
|
|
93
|
-
const TransferBatch = struct {
|
|
94
|
-
/// Index of the first transfer in the batch.
|
|
95
|
-
min: usize,
|
|
96
|
-
/// Index of the last transfer in the batch.
|
|
97
|
-
max: usize,
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
/// Indexes: [valid:bool][limit:bool][method]
|
|
101
|
-
const transfer_templates = table: {
|
|
102
|
-
const SNGL = @enumToInt(TransferPlan.Method.single_phase);
|
|
103
|
-
const PEND = @enumToInt(TransferPlan.Method.pending);
|
|
104
|
-
const POST = @enumToInt(TransferPlan.Method.post_pending);
|
|
105
|
-
const VOID = @enumToInt(TransferPlan.Method.void_pending);
|
|
106
|
-
const Result = accounting_auditor.CreateTransferResultSet;
|
|
107
|
-
const result = Result.init;
|
|
108
|
-
|
|
109
|
-
const two_phase_ok = .{
|
|
110
|
-
.ok = true,
|
|
111
|
-
.pending_transfer_already_posted = true,
|
|
112
|
-
.pending_transfer_already_voided = true,
|
|
113
|
-
.pending_transfer_expired = true,
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const limits = result(.{
|
|
117
|
-
.exceeds_credits = true,
|
|
118
|
-
.exceeds_debits = true,
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const either = struct {
|
|
122
|
-
fn either(a: Result, b: Result) Result {
|
|
123
|
-
var c = a;
|
|
124
|
-
c.setUnion(b);
|
|
125
|
-
return c;
|
|
126
|
-
}
|
|
127
|
-
}.either;
|
|
128
|
-
|
|
129
|
-
const template = struct {
|
|
130
|
-
fn template(ledger: u32, transfer_result: Result) TransferTemplate {
|
|
131
|
-
return .{
|
|
132
|
-
.ledger = ledger,
|
|
133
|
-
.result = transfer_result,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
}.template;
|
|
137
|
-
|
|
138
|
-
// [valid:bool][limit:bool][method]
|
|
139
|
-
var templates: [2][2][std.meta.fields(TransferPlan.Method).len]TransferTemplate = undefined;
|
|
140
|
-
|
|
141
|
-
// template(ledger, result)
|
|
142
|
-
templates[0][0][SNGL] = template(0, result(.{ .ledger_must_not_be_zero = true }));
|
|
143
|
-
templates[0][0][PEND] = template(0, result(.{ .ledger_must_not_be_zero = true }));
|
|
144
|
-
templates[0][0][POST] = template(9, result(.{ .pending_transfer_has_different_ledger = true }));
|
|
145
|
-
templates[0][0][VOID] = template(9, result(.{ .pending_transfer_has_different_ledger = true }));
|
|
146
|
-
|
|
147
|
-
templates[0][1][SNGL] = template(0, result(.{ .ledger_must_not_be_zero = true }));
|
|
148
|
-
templates[0][1][PEND] = template(0, result(.{ .ledger_must_not_be_zero = true }));
|
|
149
|
-
templates[0][1][POST] = template(9, result(.{ .pending_transfer_has_different_ledger = true }));
|
|
150
|
-
templates[0][1][VOID] = template(9, result(.{ .pending_transfer_has_different_ledger = true }));
|
|
151
|
-
|
|
152
|
-
templates[1][0][SNGL] = template(1, result(.{ .ok = true }));
|
|
153
|
-
templates[1][0][PEND] = template(1, result(.{ .ok = true }));
|
|
154
|
-
templates[1][0][POST] = template(1, result(two_phase_ok));
|
|
155
|
-
templates[1][0][VOID] = template(1, result(two_phase_ok));
|
|
156
|
-
|
|
157
|
-
templates[1][1][SNGL] = template(1, either(limits, result(.{ .ok = true })));
|
|
158
|
-
templates[1][1][PEND] = template(1, either(limits, result(.{ .ok = true })));
|
|
159
|
-
templates[1][1][POST] = template(1, either(limits, result(two_phase_ok)));
|
|
160
|
-
templates[1][1][VOID] = template(1, either(limits, result(two_phase_ok)));
|
|
161
|
-
|
|
162
|
-
break :table templates;
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
pub fn WorkloadType(comptime AccountingStateMachine: type) type {
|
|
166
|
-
const Operation = AccountingStateMachine.Operation;
|
|
167
|
-
|
|
168
|
-
const Action = enum(u8) {
|
|
169
|
-
create_accounts = @enumToInt(Operation.create_accounts),
|
|
170
|
-
create_transfers = @enumToInt(Operation.create_transfers),
|
|
171
|
-
lookup_accounts = @enumToInt(Operation.lookup_accounts),
|
|
172
|
-
lookup_transfers = @enumToInt(Operation.lookup_transfers),
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
return struct {
|
|
176
|
-
const Self = @This();
|
|
177
|
-
|
|
178
|
-
pub const Options = OptionsType(AccountingStateMachine, Action);
|
|
179
|
-
|
|
180
|
-
random: std.rand.Random,
|
|
181
|
-
auditor: Auditor,
|
|
182
|
-
options: Options,
|
|
183
|
-
|
|
184
|
-
transfer_plan_seed: u64,
|
|
185
|
-
|
|
186
|
-
/// Whether a `create_accounts` message has ever been sent.
|
|
187
|
-
accounts_sent: bool = false,
|
|
188
|
-
|
|
189
|
-
/// The index of the next transfer to send.
|
|
190
|
-
transfers_sent: usize = 0,
|
|
191
|
-
|
|
192
|
-
/// All transfers below this index have been delivered.
|
|
193
|
-
/// Any transfers above this index that have been delivered are stored in
|
|
194
|
-
/// `transfers_delivered_recently`.
|
|
195
|
-
transfers_delivered_past: usize = 0,
|
|
196
|
-
|
|
197
|
-
/// Track index ranges of `create_transfers` batches that have committed but are greater
|
|
198
|
-
/// than or equal to `transfers_delivered_past` (which is still in-flight).
|
|
199
|
-
transfers_delivered_recently: TransferBatchQueue,
|
|
200
|
-
|
|
201
|
-
/// Track the number of pending transfers that have been sent but not committed.
|
|
202
|
-
transfers_pending_in_flight: usize = 0,
|
|
203
|
-
|
|
204
|
-
pub fn init(allocator: std.mem.Allocator, random: std.rand.Random, options: Options) !Self {
|
|
205
|
-
assert(options.create_account_invalid_probability <= 100);
|
|
206
|
-
assert(options.create_transfer_invalid_probability <= 100);
|
|
207
|
-
assert(options.create_transfer_limit_probability <= 100);
|
|
208
|
-
assert(options.create_transfer_pending_probability <= 100);
|
|
209
|
-
assert(options.create_transfer_post_probability <= 100);
|
|
210
|
-
assert(options.create_transfer_void_probability <= 100);
|
|
211
|
-
assert(options.lookup_account_invalid_probability <= 100);
|
|
212
|
-
|
|
213
|
-
assert(options.account_limit_probability <= 100);
|
|
214
|
-
assert(options.linked_valid_probability <= 100);
|
|
215
|
-
assert(options.linked_invalid_probability <= 100);
|
|
216
|
-
|
|
217
|
-
assert(options.accounts_batch_size_span + options.accounts_batch_size_min <=
|
|
218
|
-
AccountingStateMachine.constants.batch_max.create_accounts);
|
|
219
|
-
assert(options.accounts_batch_size_span >= 1);
|
|
220
|
-
assert(options.transfers_batch_size_span + options.transfers_batch_size_min <=
|
|
221
|
-
AccountingStateMachine.constants.batch_max.create_transfers);
|
|
222
|
-
assert(options.transfers_batch_size_span >= 1);
|
|
223
|
-
|
|
224
|
-
var auditor = try Auditor.init(allocator, random, options.auditor_options);
|
|
225
|
-
errdefer auditor.deinit(allocator);
|
|
226
|
-
|
|
227
|
-
var transfers_delivered_recently = TransferBatchQueue.init(allocator, {});
|
|
228
|
-
errdefer transfers_delivered_recently.deinit();
|
|
229
|
-
try transfers_delivered_recently.ensureTotalCapacity(
|
|
230
|
-
options.auditor_options.client_count * constants.client_request_queue_max,
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
for (auditor.accounts) |*account, i| {
|
|
234
|
-
account.* = std.mem.zeroInit(tb.Account, .{
|
|
235
|
-
.id = auditor.account_index_to_id(i),
|
|
236
|
-
.ledger = 1,
|
|
237
|
-
.code = 123,
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
if (chance(random, options.account_limit_probability)) {
|
|
241
|
-
const b = random.boolean();
|
|
242
|
-
account.flags.debits_must_not_exceed_credits = b;
|
|
243
|
-
account.flags.credits_must_not_exceed_debits = !b;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return Self{
|
|
248
|
-
.random = random,
|
|
249
|
-
.auditor = auditor,
|
|
250
|
-
.options = options,
|
|
251
|
-
.transfer_plan_seed = random.int(u64),
|
|
252
|
-
.transfers_delivered_recently = transfers_delivered_recently,
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
|
|
257
|
-
self.auditor.deinit(allocator);
|
|
258
|
-
self.transfers_delivered_recently.deinit();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
pub fn done(self: *const Self) bool {
|
|
262
|
-
if (self.transfers_delivered_recently.len != 0) return false;
|
|
263
|
-
return self.auditor.done();
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/// A client may build multiple requests to queue up while another is in-flight.
|
|
267
|
-
pub fn build_request(
|
|
268
|
-
self: *Self,
|
|
269
|
-
client_index: usize,
|
|
270
|
-
body: []align(@alignOf(vsr.Header)) u8,
|
|
271
|
-
) struct {
|
|
272
|
-
operation: Operation,
|
|
273
|
-
size: usize,
|
|
274
|
-
} {
|
|
275
|
-
assert(client_index < self.auditor.options.client_count);
|
|
276
|
-
assert(body.len == constants.message_size_max - @sizeOf(vsr.Header));
|
|
277
|
-
|
|
278
|
-
const action = action: {
|
|
279
|
-
if (!self.accounts_sent and self.random.boolean()) {
|
|
280
|
-
// Early in the test make sure some accounts get created.
|
|
281
|
-
self.accounts_sent = true;
|
|
282
|
-
break :action .create_accounts;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
break :action switch (sample_distribution(self.random, self.options.operations)) {
|
|
286
|
-
.create_accounts => Action.create_accounts,
|
|
287
|
-
.create_transfers => Action.create_transfers,
|
|
288
|
-
.lookup_accounts => Action.lookup_accounts,
|
|
289
|
-
.lookup_transfers => Action.lookup_transfers,
|
|
290
|
-
};
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
const size = switch (action) {
|
|
294
|
-
.create_accounts => @sizeOf(tb.Account) *
|
|
295
|
-
self.build_create_accounts(client_index, self.batch(tb.Account, action, body)),
|
|
296
|
-
.create_transfers => @sizeOf(tb.Transfer) *
|
|
297
|
-
self.build_create_transfers(client_index, self.batch(tb.Transfer, action, body)),
|
|
298
|
-
.lookup_accounts => @sizeOf(u128) *
|
|
299
|
-
self.build_lookup_accounts(self.batch(u128, action, body)),
|
|
300
|
-
.lookup_transfers => @sizeOf(u128) *
|
|
301
|
-
self.build_lookup_transfers(self.batch(u128, action, body)),
|
|
302
|
-
};
|
|
303
|
-
assert(size <= body.len);
|
|
304
|
-
|
|
305
|
-
return .{
|
|
306
|
-
.operation = @intToEnum(Operation, @enumToInt(action)),
|
|
307
|
-
.size = size,
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/// `on_reply` is called for replies in commit order.
|
|
312
|
-
pub fn on_reply(
|
|
313
|
-
self: *Self,
|
|
314
|
-
client_index: usize,
|
|
315
|
-
operation: vsr.Operation,
|
|
316
|
-
timestamp: u64,
|
|
317
|
-
request_body: []align(@alignOf(vsr.Header)) const u8,
|
|
318
|
-
reply_body: []align(@alignOf(vsr.Header)) const u8,
|
|
319
|
-
) void {
|
|
320
|
-
assert(timestamp != 0);
|
|
321
|
-
assert(request_body.len <= constants.message_size_max - @sizeOf(vsr.Header));
|
|
322
|
-
assert(reply_body.len <= constants.message_size_max - @sizeOf(vsr.Header));
|
|
323
|
-
|
|
324
|
-
switch (operation.cast(AccountingStateMachine)) {
|
|
325
|
-
.create_accounts => self.auditor.on_create_accounts(
|
|
326
|
-
client_index,
|
|
327
|
-
timestamp,
|
|
328
|
-
std.mem.bytesAsSlice(tb.Account, request_body),
|
|
329
|
-
std.mem.bytesAsSlice(tb.CreateAccountsResult, reply_body),
|
|
330
|
-
),
|
|
331
|
-
.create_transfers => self.on_create_transfers(
|
|
332
|
-
client_index,
|
|
333
|
-
timestamp,
|
|
334
|
-
std.mem.bytesAsSlice(tb.Transfer, request_body),
|
|
335
|
-
std.mem.bytesAsSlice(tb.CreateTransfersResult, reply_body),
|
|
336
|
-
),
|
|
337
|
-
.lookup_accounts => self.auditor.on_lookup_accounts(
|
|
338
|
-
client_index,
|
|
339
|
-
timestamp,
|
|
340
|
-
std.mem.bytesAsSlice(u128, request_body),
|
|
341
|
-
std.mem.bytesAsSlice(tb.Account, reply_body),
|
|
342
|
-
),
|
|
343
|
-
.lookup_transfers => self.on_lookup_transfers(
|
|
344
|
-
client_index,
|
|
345
|
-
timestamp,
|
|
346
|
-
std.mem.bytesAsSlice(u128, request_body),
|
|
347
|
-
std.mem.bytesAsSlice(tb.Transfer, reply_body),
|
|
348
|
-
),
|
|
349
|
-
else => unreachable,
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
fn build_create_accounts(self: *Self, client_index: usize, accounts: []tb.Account) usize {
|
|
354
|
-
const results = self.auditor.expect_create_accounts(client_index);
|
|
355
|
-
for (accounts) |*account, i| {
|
|
356
|
-
const account_index = self.random.uintLessThanBiased(usize, self.auditor.accounts.len);
|
|
357
|
-
account.* = self.auditor.accounts[account_index];
|
|
358
|
-
account.debits_pending = 0;
|
|
359
|
-
account.debits_posted = 0;
|
|
360
|
-
account.credits_pending = 0;
|
|
361
|
-
account.credits_posted = 0;
|
|
362
|
-
account.timestamp = 0;
|
|
363
|
-
results[i] = accounting_auditor.CreateAccountResultSet{};
|
|
364
|
-
|
|
365
|
-
if (chance(self.random, self.options.create_account_invalid_probability)) {
|
|
366
|
-
account.ledger = 0;
|
|
367
|
-
results[i].insert(.ledger_must_not_be_zero);
|
|
368
|
-
} else {
|
|
369
|
-
if (!self.auditor.accounts_created[account_index]) {
|
|
370
|
-
results[i].insert(.ok);
|
|
371
|
-
}
|
|
372
|
-
// Even if the account doesn't exist yet, we may race another request.
|
|
373
|
-
results[i].insert(.exists);
|
|
374
|
-
}
|
|
375
|
-
assert(results[i].count() > 0);
|
|
376
|
-
}
|
|
377
|
-
return accounts.len;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
fn build_create_transfers(self: *Self, client_index: usize, transfers: []tb.Transfer) usize {
|
|
381
|
-
const results = self.auditor.expect_create_transfers(client_index);
|
|
382
|
-
var transfers_count: usize = transfers.len;
|
|
383
|
-
var i: usize = 0;
|
|
384
|
-
while (i < transfers_count) {
|
|
385
|
-
const transfer_index = self.transfers_sent;
|
|
386
|
-
const transfer_plan = self.transfer_index_to_plan(transfer_index);
|
|
387
|
-
const transfer_id = self.transfer_index_to_id(transfer_index);
|
|
388
|
-
results[i] = self.build_transfer(transfer_id, transfer_plan, &transfers[i]) orelse {
|
|
389
|
-
// This transfer index can't be built; stop with what we have so far.
|
|
390
|
-
// Hopefully it will be unblocked before the next `create_transfers`.
|
|
391
|
-
transfers_count = i;
|
|
392
|
-
break;
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
if (i != 0 and results[i].count() == 1 and results[i - 1].count() == 1) {
|
|
396
|
-
// To support random `lookup_transfers`, linked transfers can't be planned.
|
|
397
|
-
// Instead, link transfers opportunistically, when consecutive transfers can be
|
|
398
|
-
// linked without altering any of their outcomes.
|
|
399
|
-
|
|
400
|
-
if (results[i].contains(.ok) and results[i - 1].contains(.ok) and
|
|
401
|
-
chance(self.random, self.options.linked_valid_probability))
|
|
402
|
-
{
|
|
403
|
-
transfers[i - 1].flags.linked = true;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (!results[i].contains(.ok) and !results[i - 1].contains(.ok) and
|
|
407
|
-
chance(self.random, self.options.linked_invalid_probability))
|
|
408
|
-
{
|
|
409
|
-
// Convert the previous transfer to a single-phase no-limit transfer, but
|
|
410
|
-
// link it to the current transfer — it will still fail.
|
|
411
|
-
_ = self.build_transfer(transfers[i - 1].id, .{
|
|
412
|
-
.valid = true,
|
|
413
|
-
.limit = false,
|
|
414
|
-
.method = .single_phase,
|
|
415
|
-
}, &transfers[i - 1]);
|
|
416
|
-
transfers[i - 1].flags.linked = true;
|
|
417
|
-
results[i - 1] = accounting_auditor.CreateTransferResultSet.init(.{
|
|
418
|
-
.linked_event_failed = true,
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
assert(results[i].count() > 0);
|
|
423
|
-
|
|
424
|
-
if (transfers[i].flags.pending) self.transfers_pending_in_flight += 1;
|
|
425
|
-
i += 1;
|
|
426
|
-
self.transfers_sent += 1;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Checksum transfers only after the whole batch is ready.
|
|
430
|
-
// The opportunistic linking backtracks to modify transfers.
|
|
431
|
-
for (transfers[0..transfers_count]) |*transfer| {
|
|
432
|
-
transfer.user_data = vsr.checksum(std.mem.asBytes(transfer));
|
|
433
|
-
}
|
|
434
|
-
assert(transfers_count == i);
|
|
435
|
-
assert(transfers_count <= transfers.len);
|
|
436
|
-
return transfers_count;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
fn build_lookup_accounts(self: *Self, lookup_ids: []u128) usize {
|
|
440
|
-
for (lookup_ids) |*id| {
|
|
441
|
-
if (chance(self.random, self.options.lookup_account_invalid_probability)) {
|
|
442
|
-
// Pick an account with valid index (rather than "random.int(u128)") because the
|
|
443
|
-
// Auditor must decode the id to check for a matching account.
|
|
444
|
-
id.* = self.auditor.account_index_to_id(self.random.int(usize));
|
|
445
|
-
} else {
|
|
446
|
-
id.* = self.auditor.accounts[self.random.uintLessThanBiased(usize, self.auditor.accounts.len)].id;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
return lookup_ids.len;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
fn build_lookup_transfers(self: *const Self, lookup_ids: []u128) usize {
|
|
453
|
-
const delivered = self.transfers_delivered_past;
|
|
454
|
-
const lookup_window = sample_distribution(self.random, self.options.lookup_transfer);
|
|
455
|
-
const lookup_window_start = switch (lookup_window) {
|
|
456
|
-
// +1 to avoid an error when delivered=0.
|
|
457
|
-
.delivered => self.random.uintLessThanBiased(usize, delivered + 1),
|
|
458
|
-
// +1 to avoid an error when delivered=transfers_sent.
|
|
459
|
-
.sending => self.random.intRangeLessThanBiased(
|
|
460
|
-
usize,
|
|
461
|
-
delivered,
|
|
462
|
-
self.transfers_sent + 1,
|
|
463
|
-
),
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
// +1 to make the span-max inclusive.
|
|
467
|
-
const lookup_window_size = std.math.min(
|
|
468
|
-
fuzz.random_int_exponential(
|
|
469
|
-
self.random,
|
|
470
|
-
usize,
|
|
471
|
-
self.options.lookup_transfer_span_mean,
|
|
472
|
-
),
|
|
473
|
-
self.transfers_sent - lookup_window_start,
|
|
474
|
-
);
|
|
475
|
-
if (lookup_window_size == 0) return 0;
|
|
476
|
-
|
|
477
|
-
for (lookup_ids) |*lookup_id| {
|
|
478
|
-
lookup_id.* = self.transfer_index_to_id(
|
|
479
|
-
lookup_window_start + self.random.uintLessThanBiased(usize, lookup_window_size),
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
return lookup_ids.len;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/// The transfer built is guaranteed to match the TransferPlan's outcome.
|
|
486
|
-
/// The transfer built is _not_ guaranteed to match the TransferPlan's method.
|
|
487
|
-
///
|
|
488
|
-
/// Returns `null` if the transfer plan cannot be fulfilled (because there aren't enough
|
|
489
|
-
/// accounts created).
|
|
490
|
-
fn build_transfer(
|
|
491
|
-
self: *const Self,
|
|
492
|
-
transfer_id: u128,
|
|
493
|
-
transfer_plan: TransferPlan,
|
|
494
|
-
transfer: *tb.Transfer,
|
|
495
|
-
) ?accounting_auditor.CreateTransferResultSet {
|
|
496
|
-
// If the specified method is unavailable, swap it.
|
|
497
|
-
// Changing the method may narrow the TransferOutcome (unknown→success, unknown→failure)
|
|
498
|
-
// but never broaden it (success→unknown, success→failure).
|
|
499
|
-
const method = method: {
|
|
500
|
-
const default = transfer_plan.method;
|
|
501
|
-
if (default == .pending and
|
|
502
|
-
self.auditor.pending_expiries.count() + self.transfers_pending_in_flight ==
|
|
503
|
-
self.auditor.options.transfers_pending_max)
|
|
504
|
-
{
|
|
505
|
-
break :method .single_phase;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
if (default == .post_pending or default == .void_pending) {
|
|
509
|
-
if (self.auditor.pending_transfers.count() == 0) {
|
|
510
|
-
break :method .single_phase;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
break :method default;
|
|
514
|
-
};
|
|
515
|
-
|
|
516
|
-
const index_valid = @boolToInt(transfer_plan.valid);
|
|
517
|
-
const index_limit = @boolToInt(transfer_plan.limit);
|
|
518
|
-
const index_method = @enumToInt(method);
|
|
519
|
-
const transfer_template = &transfer_templates[index_valid][index_limit][index_method];
|
|
520
|
-
|
|
521
|
-
const limit_debits = transfer_plan.limit and self.random.boolean();
|
|
522
|
-
const limit_credits = transfer_plan.limit and (self.random.boolean() or !limit_debits);
|
|
523
|
-
|
|
524
|
-
const debit_account = self.auditor.pick_account(.{
|
|
525
|
-
.created = true,
|
|
526
|
-
.debits_must_not_exceed_credits = limit_debits,
|
|
527
|
-
.credits_must_not_exceed_debits = null,
|
|
528
|
-
}) orelse return null;
|
|
529
|
-
|
|
530
|
-
const credit_account = self.auditor.pick_account(.{
|
|
531
|
-
.created = true,
|
|
532
|
-
.debits_must_not_exceed_credits = null,
|
|
533
|
-
.credits_must_not_exceed_debits = limit_credits,
|
|
534
|
-
.exclude = debit_account.id,
|
|
535
|
-
}) orelse return null;
|
|
536
|
-
|
|
537
|
-
transfer.* = .{
|
|
538
|
-
.id = transfer_id,
|
|
539
|
-
.debit_account_id = debit_account.id,
|
|
540
|
-
.credit_account_id = credit_account.id,
|
|
541
|
-
// "user_data" will be set to a checksum of the Transfer.
|
|
542
|
-
.user_data = 0,
|
|
543
|
-
.reserved = 0,
|
|
544
|
-
.pending_id = 0,
|
|
545
|
-
.timeout = 0,
|
|
546
|
-
.ledger = transfer_template.ledger,
|
|
547
|
-
.code = 123,
|
|
548
|
-
.flags = .{},
|
|
549
|
-
// +1 to avoid `.amount_must_not_be_zero`.
|
|
550
|
-
.amount = 1 + @as(u64, self.random.int(u8)),
|
|
551
|
-
};
|
|
552
|
-
|
|
553
|
-
switch (method) {
|
|
554
|
-
.single_phase => {},
|
|
555
|
-
.pending => {
|
|
556
|
-
transfer.flags = .{ .pending = true };
|
|
557
|
-
// Bound the timeout to ensure we never hit `overflows_timeout`.
|
|
558
|
-
transfer.timeout = 1 + std.math.min(
|
|
559
|
-
std.math.maxInt(u64) / 2,
|
|
560
|
-
fuzz.random_int_exponential(
|
|
561
|
-
self.random,
|
|
562
|
-
u64,
|
|
563
|
-
self.options.pending_timeout_mean,
|
|
564
|
-
),
|
|
565
|
-
);
|
|
566
|
-
},
|
|
567
|
-
.post_pending, .void_pending => {
|
|
568
|
-
// Don't depend on `HashMap.keyIterator()` being deterministic.
|
|
569
|
-
// Pick a random "target" key, then post/void the id it is nearest to.
|
|
570
|
-
const target = self.random.int(u128);
|
|
571
|
-
var previous: ?u128 = null;
|
|
572
|
-
var iterator = self.auditor.pending_transfers.keyIterator();
|
|
573
|
-
while (iterator.next()) |id| {
|
|
574
|
-
if (previous == null or
|
|
575
|
-
std.math.max(target, id.*) - std.math.min(target, id.*) <
|
|
576
|
-
std.math.max(target, previous.?) - std.math.min(target, previous.?))
|
|
577
|
-
{
|
|
578
|
-
previous = id.*;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// If there were no pending ids, the method would have been changed.
|
|
583
|
-
const pending_id = previous.?;
|
|
584
|
-
const pending_transfer = self.auditor.pending_transfers.getPtr(previous.?).?;
|
|
585
|
-
const dr = pending_transfer.debit_account_index;
|
|
586
|
-
const cr = pending_transfer.credit_account_index;
|
|
587
|
-
// Don't use the default '0' parameters because the StateMachine overwrites 0s
|
|
588
|
-
// with the pending transfer's values, invalidating the post/void transfer checksum.
|
|
589
|
-
transfer.debit_account_id = self.auditor.account_index_to_id(dr);
|
|
590
|
-
transfer.credit_account_id = self.auditor.account_index_to_id(cr);
|
|
591
|
-
if (method == .post_pending) {
|
|
592
|
-
// 1+rng for minimum amount of 1. This also makes the pending amount inclusive.
|
|
593
|
-
transfer.amount = 1 + self.random.uintLessThanBiased(u64, pending_transfer.amount);
|
|
594
|
-
} else {
|
|
595
|
-
transfer.amount = pending_transfer.amount;
|
|
596
|
-
}
|
|
597
|
-
transfer.pending_id = pending_id;
|
|
598
|
-
transfer.flags = .{
|
|
599
|
-
.post_pending_transfer = method == .post_pending,
|
|
600
|
-
.void_pending_transfer = method == .void_pending,
|
|
601
|
-
};
|
|
602
|
-
},
|
|
603
|
-
}
|
|
604
|
-
assert(transfer_template.result.count() > 0);
|
|
605
|
-
return transfer_template.result;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
fn batch(self: *const Self, comptime T: type, action: Action, body: []align(@alignOf(vsr.Header)) u8) []T {
|
|
609
|
-
const batch_min = switch (action) {
|
|
610
|
-
.create_accounts, .lookup_accounts => self.options.accounts_batch_size_min,
|
|
611
|
-
.create_transfers, .lookup_transfers => self.options.transfers_batch_size_min,
|
|
612
|
-
};
|
|
613
|
-
const batch_span = switch (action) {
|
|
614
|
-
.create_accounts, .lookup_accounts => self.options.accounts_batch_size_span,
|
|
615
|
-
.create_transfers, .lookup_transfers => self.options.transfers_batch_size_span,
|
|
616
|
-
};
|
|
617
|
-
|
|
618
|
-
// +1 because the span is inclusive.
|
|
619
|
-
const batch_size = batch_min + self.random.uintLessThanBiased(usize, batch_span + 1);
|
|
620
|
-
return std.mem.bytesAsSlice(T, body)[0..batch_size];
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
fn transfer_id_to_index(self: *const Self, id: u128) usize {
|
|
624
|
-
// -1 because id=0 is not valid, so index=0→id=1.
|
|
625
|
-
return @intCast(usize, self.options.transfer_id_permutation.decode(id)) - 1;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
fn transfer_index_to_id(self: *const Self, index: usize) u128 {
|
|
629
|
-
// +1 so that index=0 is encoded as a valid id.
|
|
630
|
-
return self.options.transfer_id_permutation.encode(index + 1);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/// To support `lookup_transfers`, the `TransferPlan` is deterministic based on:
|
|
634
|
-
/// * `Workload.transfer_plan_seed`, and
|
|
635
|
-
/// * the transfer `index`.
|
|
636
|
-
fn transfer_index_to_plan(self: *const Self, index: usize) TransferPlan {
|
|
637
|
-
var prng = std.rand.DefaultPrng.init(self.transfer_plan_seed ^ @as(u64, index));
|
|
638
|
-
const random = prng.random();
|
|
639
|
-
const method: TransferPlan.Method = blk: {
|
|
640
|
-
if (chance(random, self.options.create_transfer_pending_probability)) {
|
|
641
|
-
break :blk .pending;
|
|
642
|
-
}
|
|
643
|
-
if (chance(random, self.options.create_transfer_post_probability)) {
|
|
644
|
-
break :blk .post_pending;
|
|
645
|
-
}
|
|
646
|
-
if (chance(random, self.options.create_transfer_void_probability)) {
|
|
647
|
-
break :blk .void_pending;
|
|
648
|
-
}
|
|
649
|
-
break :blk .single_phase;
|
|
650
|
-
};
|
|
651
|
-
return .{
|
|
652
|
-
.valid = !chance(random, self.options.create_transfer_invalid_probability),
|
|
653
|
-
.limit = chance(random, self.options.create_transfer_limit_probability),
|
|
654
|
-
.method = method,
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
fn on_create_transfers(
|
|
659
|
-
self: *Self,
|
|
660
|
-
client_index: usize,
|
|
661
|
-
timestamp: u64,
|
|
662
|
-
transfers: []const tb.Transfer,
|
|
663
|
-
results_sparse: []const tb.CreateTransfersResult,
|
|
664
|
-
) void {
|
|
665
|
-
self.auditor.on_create_transfers(client_index, timestamp, transfers, results_sparse);
|
|
666
|
-
if (transfers.len == 0) return;
|
|
667
|
-
|
|
668
|
-
const transfer_index_min = self.transfer_id_to_index(transfers[0].id);
|
|
669
|
-
const transfer_index_max = self.transfer_id_to_index(transfers[transfers.len - 1].id);
|
|
670
|
-
assert(transfer_index_min <= transfer_index_max);
|
|
671
|
-
|
|
672
|
-
self.transfers_delivered_recently.add(.{
|
|
673
|
-
.min = transfer_index_min,
|
|
674
|
-
.max = transfer_index_max,
|
|
675
|
-
}) catch unreachable;
|
|
676
|
-
|
|
677
|
-
while (self.transfers_delivered_recently.peek()) |delivered| {
|
|
678
|
-
if (self.transfers_delivered_past == delivered.min) {
|
|
679
|
-
self.transfers_delivered_past = delivered.max + 1;
|
|
680
|
-
_ = self.transfers_delivered_recently.remove();
|
|
681
|
-
} else {
|
|
682
|
-
assert(self.transfers_delivered_past < delivered.min);
|
|
683
|
-
break;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
for (transfers) |*transfer| {
|
|
688
|
-
if (transfer.flags.pending) self.transfers_pending_in_flight -= 1;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
fn on_lookup_transfers(
|
|
693
|
-
self: *Self,
|
|
694
|
-
client_index: usize,
|
|
695
|
-
timestamp: u64,
|
|
696
|
-
ids: []const u128,
|
|
697
|
-
results: []const tb.Transfer,
|
|
698
|
-
) void {
|
|
699
|
-
self.auditor.on_lookup_transfers(client_index, timestamp, ids, results);
|
|
700
|
-
|
|
701
|
-
var transfers = accounting_auditor.IteratorForLookup(tb.Transfer).init(results);
|
|
702
|
-
for (ids) |transfer_id| {
|
|
703
|
-
const transfer_index = self.transfer_id_to_index(transfer_id);
|
|
704
|
-
const transfer_outcome = self.transfer_index_to_plan(transfer_index).outcome();
|
|
705
|
-
const result = transfers.take(transfer_id);
|
|
706
|
-
|
|
707
|
-
if (result) |transfer| {
|
|
708
|
-
// The transfer exists; verify its integrity.
|
|
709
|
-
const checksum_actual = transfer.user_data;
|
|
710
|
-
var check = transfer.*;
|
|
711
|
-
check.user_data = 0;
|
|
712
|
-
check.timestamp = 0;
|
|
713
|
-
const checksum_expect = vsr.checksum(std.mem.asBytes(&check));
|
|
714
|
-
assert(checksum_expect == checksum_actual);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
if (transfer_index >= self.transfers_sent) {
|
|
718
|
-
// This transfer hasn't been created yet.
|
|
719
|
-
assert(result == null);
|
|
720
|
-
continue;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
switch (transfer_outcome) {
|
|
724
|
-
.success => {
|
|
725
|
-
if (transfer_index < self.transfers_delivered_past) {
|
|
726
|
-
// The transfer was delivered; it must exist.
|
|
727
|
-
assert(result != null);
|
|
728
|
-
} else {
|
|
729
|
-
for (self.transfers_delivered_recently.items) |delivered| {
|
|
730
|
-
if (transfer_index >= delivered.min and
|
|
731
|
-
transfer_index <= delivered.max)
|
|
732
|
-
{
|
|
733
|
-
// The transfer was delivered recently; it must exist.
|
|
734
|
-
assert(result != null);
|
|
735
|
-
break;
|
|
736
|
-
}
|
|
737
|
-
} else {
|
|
738
|
-
// The `create_transfers` has not committed (it may be in-flight).
|
|
739
|
-
assert(result == null);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
},
|
|
743
|
-
// An invalid transfer is never persisted.
|
|
744
|
-
.failure => assert(result == null),
|
|
745
|
-
// Due to races and timeouts, these transfer types are not guaranteed to succeed.
|
|
746
|
-
.unknown => {},
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
};
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
fn OptionsType(comptime StateMachine: type, comptime Action: type) type {
|
|
754
|
-
return struct {
|
|
755
|
-
const Options = @This();
|
|
756
|
-
|
|
757
|
-
auditor_options: Auditor.Options,
|
|
758
|
-
transfer_id_permutation: IdPermutation,
|
|
759
|
-
|
|
760
|
-
operations: std.enums.EnumFieldStruct(Action, usize, null),
|
|
761
|
-
|
|
762
|
-
create_account_invalid_probability: u8, // ≤ 100
|
|
763
|
-
create_transfer_invalid_probability: u8, // ≤ 100
|
|
764
|
-
create_transfer_limit_probability: u8, // ≤ 100
|
|
765
|
-
create_transfer_pending_probability: u8, // ≤ 100
|
|
766
|
-
create_transfer_post_probability: u8, // ≤ 100
|
|
767
|
-
create_transfer_void_probability: u8, // ≤ 100
|
|
768
|
-
lookup_account_invalid_probability: u8, // ≤ 100
|
|
769
|
-
|
|
770
|
-
lookup_transfer: std.enums.EnumFieldStruct(enum {
|
|
771
|
-
/// Query a transfer that has either been committed or rejected.
|
|
772
|
-
delivered,
|
|
773
|
-
/// Query a transfer whose `create_transfers` is in-flight.
|
|
774
|
-
sending,
|
|
775
|
-
}, usize, null),
|
|
776
|
-
|
|
777
|
-
// Size of timespan for querying, measured in transfers
|
|
778
|
-
lookup_transfer_span_mean: usize,
|
|
779
|
-
|
|
780
|
-
account_limit_probability: u8, // ≤ 100
|
|
781
|
-
|
|
782
|
-
/// This probability is only checked for consecutive guaranteed-successful transfers.
|
|
783
|
-
linked_valid_probability: u8,
|
|
784
|
-
/// This probability is only checked for consecutive invalid transfers.
|
|
785
|
-
linked_invalid_probability: u8,
|
|
786
|
-
|
|
787
|
-
pending_timeout_mean: u64,
|
|
788
|
-
|
|
789
|
-
accounts_batch_size_min: usize,
|
|
790
|
-
accounts_batch_size_span: usize, // inclusive
|
|
791
|
-
transfers_batch_size_min: usize,
|
|
792
|
-
transfers_batch_size_span: usize, // inclusive
|
|
793
|
-
|
|
794
|
-
pub fn generate(random: std.rand.Random, options: struct {
|
|
795
|
-
client_count: usize,
|
|
796
|
-
in_flight_max: usize,
|
|
797
|
-
}) Options {
|
|
798
|
-
return .{
|
|
799
|
-
.auditor_options = .{
|
|
800
|
-
.accounts_max = 2 + random.uintLessThan(usize, 128),
|
|
801
|
-
.account_id_permutation = IdPermutation.generate(random),
|
|
802
|
-
.client_count = options.client_count,
|
|
803
|
-
.transfers_pending_max = 256,
|
|
804
|
-
.in_flight_max = options.in_flight_max,
|
|
805
|
-
},
|
|
806
|
-
.transfer_id_permutation = IdPermutation.generate(random),
|
|
807
|
-
.operations = .{
|
|
808
|
-
.create_accounts = 1 + random.uintLessThan(usize, 10),
|
|
809
|
-
.create_transfers = 1 + random.uintLessThan(usize, 100),
|
|
810
|
-
.lookup_accounts = 1 + random.uintLessThan(usize, 20),
|
|
811
|
-
.lookup_transfers = 1 + random.uintLessThan(usize, 20),
|
|
812
|
-
},
|
|
813
|
-
.create_account_invalid_probability = 1,
|
|
814
|
-
.create_transfer_invalid_probability = 1,
|
|
815
|
-
.create_transfer_limit_probability = random.uintLessThan(u8, 101),
|
|
816
|
-
.create_transfer_pending_probability = 1 + random.uintLessThan(u8, 100),
|
|
817
|
-
.create_transfer_post_probability = 1 + random.uintLessThan(u8, 50),
|
|
818
|
-
.create_transfer_void_probability = 1 + random.uintLessThan(u8, 50),
|
|
819
|
-
.lookup_account_invalid_probability = 1,
|
|
820
|
-
.lookup_transfer = .{
|
|
821
|
-
.delivered = 1 + random.uintLessThan(usize, 10),
|
|
822
|
-
.sending = 1 + random.uintLessThan(usize, 10),
|
|
823
|
-
},
|
|
824
|
-
.lookup_transfer_span_mean = 10 + random.uintLessThan(usize, 1000),
|
|
825
|
-
.account_limit_probability = random.uintLessThan(u8, 80),
|
|
826
|
-
.linked_valid_probability = random.uintLessThan(u8, 101),
|
|
827
|
-
// 100% chance because this only applies to consecutive invalid transfers, which are rare.
|
|
828
|
-
.linked_invalid_probability = 100,
|
|
829
|
-
// TODO(Timeouts): When timeouts are implemented in the StateMachine, change this to the
|
|
830
|
-
// (commented out) value so that timeouts can actually trigger.
|
|
831
|
-
.pending_timeout_mean = std.math.maxInt(u64) / 2,
|
|
832
|
-
// .pending_timeout_mean = 1 + random.uintLessThan(usize, 1_000_000_000 / 4),
|
|
833
|
-
.accounts_batch_size_min = 0,
|
|
834
|
-
.accounts_batch_size_span = 1 + random.uintLessThan(
|
|
835
|
-
usize,
|
|
836
|
-
StateMachine.constants.batch_max.create_accounts,
|
|
837
|
-
),
|
|
838
|
-
.transfers_batch_size_min = 0,
|
|
839
|
-
.transfers_batch_size_span = 1 + random.uintLessThan(
|
|
840
|
-
usize,
|
|
841
|
-
StateMachine.constants.batch_max.create_transfers,
|
|
842
|
-
),
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
};
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
/// Sample from a discrete distribution.
|
|
849
|
-
/// Use integers instead of floating-point numbers to avoid nondeterminism on different hardware.
|
|
850
|
-
fn sample_distribution(
|
|
851
|
-
random: std.rand.Random,
|
|
852
|
-
distribution: anytype,
|
|
853
|
-
) std.meta.FieldEnum(@TypeOf(distribution)) {
|
|
854
|
-
const SampleSpace = std.meta.FieldEnum(@TypeOf(distribution));
|
|
855
|
-
const Indexer = std.enums.EnumIndexer(SampleSpace);
|
|
856
|
-
|
|
857
|
-
var sum = sum: {
|
|
858
|
-
var sum: usize = 0;
|
|
859
|
-
comptime var i: usize = 0;
|
|
860
|
-
inline while (i < Indexer.count) : (i += 1) {
|
|
861
|
-
const key = comptime @tagName(Indexer.keyForIndex(i));
|
|
862
|
-
sum += @field(distribution, key);
|
|
863
|
-
}
|
|
864
|
-
break :sum sum;
|
|
865
|
-
};
|
|
866
|
-
|
|
867
|
-
var pick = random.uintLessThanBiased(usize, sum);
|
|
868
|
-
comptime var i: usize = 0;
|
|
869
|
-
inline while (i < Indexer.count) : (i += 1) {
|
|
870
|
-
const event = comptime Indexer.keyForIndex(i);
|
|
871
|
-
const weight = @field(distribution, @tagName(event));
|
|
872
|
-
if (pick < weight) return event;
|
|
873
|
-
pick -= weight;
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
@panic("sample_discrete: empty sample space");
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
/// Returns true, `p` percent of the time, else false.
|
|
880
|
-
fn chance(random: std.rand.Random, p: u8) bool {
|
|
881
|
-
assert(p <= 100);
|
|
882
|
-
return random.uintLessThanBiased(u8, 100) < p;
|
|
883
|
-
}
|