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.
|
|
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": "
|
|
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
|
|
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
|
+
});
|
package/tests/unit/unit.test.js
CHANGED
|
@@ -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');
|