trac-peer 0.4.8 → 0.4.9

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "trac-peer",
3
3
  "main": "src/index.js",
4
- "version": "0.4.8",
4
+ "version": "0.4.9",
5
5
  "type": "module",
6
6
  "pear": {
7
7
  "name": "trac-peer",
@@ -99,7 +99,7 @@
99
99
  "timers": "npm:bare-node-timers",
100
100
  "tls": "npm:bare-node-tls",
101
101
  "trac-crypto-api": "^0.1.5",
102
- "trac-msb": "^0.2.19",
102
+ "trac-msb": "github:Trac-Systems/main_settlement_bus#7220c257d8c493456714791e03f7021dbcfc7c85",
103
103
  "trac-wallet": "^1.0.4",
104
104
  "url": "npm:bare-node-url",
105
105
  "util": "npm:bare-node-util",
@@ -20,9 +20,36 @@ class Contract {
20
20
  this.value = null;
21
21
  this.assert = assert;
22
22
  this.check = new Check();
23
+ this.execute_queue = null;
24
+ }
25
+
26
+ /**
27
+ * A contract instance keeps the state of the running call (address,
28
+ * validator_address, is_feature, is_message, tx, op, value, storage) in
29
+ * instance fields, and handlers re-read those fields after every await. Two
30
+ * executions in flight on one instance therefore interleave their working
31
+ * state, which on a busy peer produced a simulation result with fields
32
+ * missing and a "get(key): storage undefined" throw in the call that resumed
33
+ * afterwards. Autobase apply is sequential, but Protocol.simulateTransaction
34
+ * executes on the same live instance from outside the apply loop, so calls
35
+ * are queued here and the body in executeQueued() runs one call at a time.
36
+ */
37
+ async execute(op, storage){
38
+ const previous = this.execute_queue === null ? Promise.resolve() : this.execute_queue;
39
+ let finish = null;
40
+ const finished = new Promise((resolve) => { finish = resolve; });
41
+ const queued = previous.then(() => finished);
42
+ this.execute_queue = queued;
43
+ await previous;
44
+ try {
45
+ return await this.executeQueued(op, storage);
46
+ } finally {
47
+ finish();
48
+ if(this.execute_queue === queued) this.execute_queue = null;
49
+ }
23
50
  }
24
51
 
25
- async execute(op, storage){
52
+ async executeQueued(op, storage){
26
53
  this.address = null;
27
54
  this.validator_address = null;
28
55
  this.is_message = false;
@@ -0,0 +1,266 @@
1
+ import test from "brittle";
2
+
3
+ import Contract from "../../src/artifacts/contract.js";
4
+
5
+ const makeProtocolStubForContract = () => {
6
+ const compile = (_schema) => () => true;
7
+ return { peer: { check: { validator: { compile } } } };
8
+ };
9
+
10
+ class MemoryStorage {
11
+ constructor(initial = {}) {
12
+ this.values = new Map(Object.entries(initial));
13
+ }
14
+
15
+ async get(key) {
16
+ return this.values.has(key) ? { value: this.values.get(key) } : null;
17
+ }
18
+
19
+ async put(key, value) {
20
+ this.values.set(key, value);
21
+ }
22
+
23
+ async del(key) {
24
+ this.values.delete(key);
25
+ }
26
+
27
+ keys() {
28
+ return [...this.values.keys()].sort();
29
+ }
30
+
31
+ snapshotBytes() {
32
+ return JSON.stringify(
33
+ [...this.values.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
34
+ );
35
+ }
36
+ }
37
+
38
+ const txKey = (n) => n.toString(16).padStart(64, "0");
39
+ const address = (n) => n.toString(16).repeat(64).slice(0, 64);
40
+
41
+ const operation = (type, value, sender, txNo, writer = "0".repeat(64)) => ({
42
+ type: "tx",
43
+ key: txKey(txNo),
44
+ value: {
45
+ dispatch: { type, value },
46
+ ipk: sender,
47
+ wp: writer,
48
+ },
49
+ });
50
+
51
+ const deferred = () => {
52
+ let resolve = null;
53
+ const promise = new Promise((settle) => { resolve = settle; });
54
+ return { promise, resolve };
55
+ };
56
+
57
+ // Give a pending execution enough turns of the event loop to reach its await.
58
+ const settle = async () => {
59
+ for (let index = 0; index < 8; index += 1) {
60
+ await new Promise((resolve) => setTimeout(resolve, 0));
61
+ }
62
+ };
63
+
64
+ const probeContract = (release, ContractClass = Contract) => {
65
+ const contract = new ContractClass(makeProtocolStubForContract(), {});
66
+ contract.addFunction("probe");
67
+ contract.probe = async function probe() {
68
+ const entered = {
69
+ tag: this.value.tag,
70
+ tx: this.tx,
71
+ address: this.address,
72
+ validator_address: this.validator_address,
73
+ type: this.op.type,
74
+ is_feature: this.is_feature,
75
+ is_message: this.is_message,
76
+ };
77
+ if (entered.tag === "held") await release.promise;
78
+ const resumed = {
79
+ tag: this.value.tag,
80
+ tx: this.tx,
81
+ address: this.address,
82
+ validator_address: this.validator_address,
83
+ type: this.op.type,
84
+ is_feature: this.is_feature,
85
+ is_message: this.is_message,
86
+ };
87
+ await this.put(`probe/${resumed.tag}`, resumed);
88
+ const stored = await this.get(`probe/${resumed.tag}`);
89
+ return { ok: true, entered, resumed, stored };
90
+ };
91
+ return contract;
92
+ };
93
+
94
+ test("contract execute: overlapping executions each keep their own operation, sender and storage", async (t) => {
95
+ const release = deferred();
96
+ const contract = probeContract(release);
97
+ const heldStorage = new MemoryStorage();
98
+ const otherStorage = new MemoryStorage();
99
+
100
+ const heldRun = contract.execute(
101
+ operation("probe", { tag: "held" }, address(1), 701, address(9)),
102
+ heldStorage
103
+ );
104
+ await settle();
105
+ const otherRun = contract.execute(
106
+ operation("probe", { tag: "other" }, address(2), 702, address(8)),
107
+ otherStorage
108
+ );
109
+ await settle();
110
+ release.resolve();
111
+ const [heldResult, otherResult] = await Promise.all([heldRun, otherRun]);
112
+
113
+ const heldExpected = {
114
+ tag: "held",
115
+ tx: txKey(701),
116
+ address: address(1),
117
+ validator_address: address(9),
118
+ type: "probe",
119
+ is_feature: false,
120
+ is_message: false,
121
+ };
122
+ const otherExpected = {
123
+ tag: "other",
124
+ tx: txKey(702),
125
+ address: address(2),
126
+ validator_address: address(8),
127
+ type: "probe",
128
+ is_feature: false,
129
+ is_message: false,
130
+ };
131
+
132
+ t.alike(heldResult.entered, heldExpected);
133
+ t.alike(heldResult.resumed, heldExpected);
134
+ t.alike(heldResult.stored, heldExpected);
135
+ t.alike(otherResult.entered, otherExpected);
136
+ t.alike(otherResult.resumed, otherExpected);
137
+ t.alike(otherResult.stored, otherExpected);
138
+ t.alike(heldStorage.keys(), ["probe/held"]);
139
+ t.alike(otherStorage.keys(), ["probe/other"]);
140
+
141
+ // The same two calls, run one after the other, produce the same results and
142
+ // the same storage bytes.
143
+ const sequentialRelease = deferred();
144
+ sequentialRelease.resolve();
145
+ const sequentialContract = probeContract(sequentialRelease);
146
+ const sequentialHeldStorage = new MemoryStorage();
147
+ const sequentialOtherStorage = new MemoryStorage();
148
+ const sequentialHeld = await sequentialContract.execute(
149
+ operation("probe", { tag: "held" }, address(1), 701, address(9)),
150
+ sequentialHeldStorage
151
+ );
152
+ const sequentialOther = await sequentialContract.execute(
153
+ operation("probe", { tag: "other" }, address(2), 702, address(8)),
154
+ sequentialOtherStorage
155
+ );
156
+
157
+ t.alike(heldResult, sequentialHeld);
158
+ t.alike(otherResult, sequentialOther);
159
+ t.is(heldStorage.snapshotBytes(), sequentialHeldStorage.snapshotBytes());
160
+ t.is(otherStorage.snapshotBytes(), sequentialOtherStorage.snapshotBytes());
161
+ t.is(contract.storage, null);
162
+ t.is(contract.address, null);
163
+ t.is(contract.execute_queue, null);
164
+ });
165
+
166
+ test("contract execute: a throwing handler leaves the queue usable", async (t) => {
167
+ const contract = new Contract(makeProtocolStubForContract(), {});
168
+ contract.addFunction("boom");
169
+ contract.boom = async function boom() {
170
+ await new Promise((resolve) => setTimeout(resolve, 0));
171
+ throw new Error("handler failed");
172
+ };
173
+ contract.addFunction("after");
174
+ contract.after = async function after() {
175
+ await this.put("after/ran", { tx: this.tx, address: this.address });
176
+ return { ok: true, tx: this.tx, address: this.address };
177
+ };
178
+
179
+ const storage = new MemoryStorage();
180
+ await t.exception(
181
+ () => contract.execute(operation("boom", { tag: "boom" }, address(3), 801), storage),
182
+ /Error in contract/
183
+ );
184
+ t.is(contract.execute_queue, null);
185
+
186
+ const result = await contract.execute(
187
+ operation("after", { tag: "after" }, address(4), 802),
188
+ storage
189
+ );
190
+ t.alike(result, { ok: true, tx: txKey(802), address: address(4) });
191
+ t.alike(storage.keys(), ["after/ran"]);
192
+
193
+ // Two more overlapping calls still take turns after the failure.
194
+ const release = deferred();
195
+ const slowStorage = new MemoryStorage();
196
+ contract.addFunction("slow");
197
+ contract.slow = async function slow() {
198
+ const seen = { tx: this.tx, address: this.address };
199
+ if (this.value.tag === "first") await release.promise;
200
+ return { ok: true, seen, still: { tx: this.tx, address: this.address } };
201
+ };
202
+ const first = contract.execute(
203
+ operation("slow", { tag: "first" }, address(5), 803),
204
+ slowStorage
205
+ );
206
+ await settle();
207
+ const second = contract.execute(
208
+ operation("slow", { tag: "second" }, address(6), 804),
209
+ slowStorage
210
+ );
211
+ await settle();
212
+ release.resolve();
213
+ const [firstResult, secondResult] = await Promise.all([first, second]);
214
+ t.alike(firstResult.seen, firstResult.still);
215
+ t.alike(secondResult.seen, secondResult.still);
216
+ t.is(firstResult.seen.address, address(5));
217
+ t.is(secondResult.seen.address, address(6));
218
+ });
219
+
220
+ test("contract execute: a sub-class override calling super.execute() still runs", async (t) => {
221
+ const calls = [];
222
+
223
+ class SubContract extends Contract {
224
+ async execute(op, storage) {
225
+ calls.push(op.value.dispatch.value.tag);
226
+ this.last_type = op.type;
227
+ try {
228
+ return await super.execute(op, storage);
229
+ } finally {
230
+ this.last_type = null;
231
+ }
232
+ }
233
+ }
234
+
235
+ const release = deferred();
236
+ const contract = probeContract(release, SubContract);
237
+ const heldStorage = new MemoryStorage();
238
+ const otherStorage = new MemoryStorage();
239
+
240
+ const heldRun = contract.execute(
241
+ operation("probe", { tag: "held" }, address(1), 901, address(9)),
242
+ heldStorage
243
+ );
244
+ await settle();
245
+ const otherRun = contract.execute(
246
+ operation("probe", { tag: "other" }, address(2), 902, address(8)),
247
+ otherStorage
248
+ );
249
+ await settle();
250
+ release.resolve();
251
+ const [heldResult, otherResult] = await Promise.all([heldRun, otherRun]);
252
+
253
+ t.alike(calls, ["held", "other"]);
254
+ t.is(heldResult.ok, true);
255
+ t.is(otherResult.ok, true);
256
+ t.alike(heldResult.entered, heldResult.resumed);
257
+ t.alike(otherResult.entered, otherResult.resumed);
258
+ t.is(heldResult.resumed.address, address(1));
259
+ t.is(otherResult.resumed.address, address(2));
260
+ t.is(heldResult.resumed.tx, txKey(901));
261
+ t.is(otherResult.resumed.tx, txKey(902));
262
+ t.alike(heldStorage.keys(), ["probe/held"]);
263
+ t.alike(otherStorage.keys(), ["probe/other"]);
264
+ t.is(contract.last_type, null);
265
+ t.is(contract.execute_queue, null);
266
+ });
@@ -4,6 +4,7 @@ import test from 'brittle';
4
4
  test.pause();
5
5
  await import('./applyGuards.test.js');
6
6
  await import('./baseContractProtocol.test.js');
7
+ await import('./contractExecuteQueue.test.js');
7
8
  await import('./cliTx.test.js');
8
9
  await import('./operations.test.js');
9
10
  await import('./simFunds.test.js');