trac-peer 0.4.7 → 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 +2 -2
- package/src/artifacts/contract.js +28 -1
- package/src/config/config.js +11 -0
- package/src/config/env.js +3 -0
- package/src/msbClient.js +26 -2
- package/src/operations/tx/index.js +5 -0
- package/tests/unit/applyGuards.test.js +59 -0
- package/tests/unit/contractExecuteQueue.test.js +266 -0
- package/tests/unit/unit.test.js +1 -0
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;
|
package/src/config/config.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import b4a from "b4a";
|
|
2
2
|
import path from "path";
|
|
3
3
|
|
|
4
|
+
const DEFAULT_MAX_MSB_SIGNED_LENGTH_FUTURE_DELTA = 1_000_000;
|
|
5
|
+
|
|
4
6
|
export class Config {
|
|
5
7
|
#options;
|
|
6
8
|
|
|
@@ -47,6 +49,15 @@ export class Config {
|
|
|
47
49
|
}
|
|
48
50
|
this.maxMsbSignedLength = maxMsbSignedLength;
|
|
49
51
|
|
|
52
|
+
const maxMsbSignedLengthFutureDelta =
|
|
53
|
+
this.#select("maxMsbSignedLengthFutureDelta", options, defaults) ??
|
|
54
|
+
DEFAULT_MAX_MSB_SIGNED_LENGTH_FUTURE_DELTA;
|
|
55
|
+
if (!Number.isSafeInteger(maxMsbSignedLengthFutureDelta) ||
|
|
56
|
+
maxMsbSignedLengthFutureDelta < 0) {
|
|
57
|
+
throw new Error("Peer: maxMsbSignedLengthFutureDelta must be a non-negative safe integer.");
|
|
58
|
+
}
|
|
59
|
+
this.maxMsbSignedLengthFutureDelta = maxMsbSignedLengthFutureDelta;
|
|
60
|
+
|
|
50
61
|
const maxMsbApplyOperationBytes = this.#select("maxMsbApplyOperationBytes", options, defaults);
|
|
51
62
|
if (!Number.isSafeInteger(maxMsbApplyOperationBytes)) {
|
|
52
63
|
throw new Error("Peer: maxMsbApplyOperationBytes must be a safe integer.");
|
package/src/config/env.js
CHANGED
|
@@ -15,6 +15,7 @@ const configData = {
|
|
|
15
15
|
txPoolMaxSize: 1_000,
|
|
16
16
|
maxTxDelay: 60,
|
|
17
17
|
maxMsbSignedLength: 1_000_000_000,
|
|
18
|
+
maxMsbSignedLengthFutureDelta: 1_000_000,
|
|
18
19
|
maxMsbApplyOperationBytes: 1024 * 1024,
|
|
19
20
|
enableInteractiveMode: true,
|
|
20
21
|
enableBackgroundTasks: true,
|
|
@@ -39,6 +40,7 @@ const configData = {
|
|
|
39
40
|
txPoolMaxSize: 1_000,
|
|
40
41
|
maxTxDelay: 60,
|
|
41
42
|
maxMsbSignedLength: 1_000_000_000,
|
|
43
|
+
maxMsbSignedLengthFutureDelta: 1_000_000,
|
|
42
44
|
maxMsbApplyOperationBytes: 1024 * 1024,
|
|
43
45
|
enableInteractiveMode: true,
|
|
44
46
|
enableBackgroundTasks: true,
|
|
@@ -63,6 +65,7 @@ const configData = {
|
|
|
63
65
|
txPoolMaxSize: 1_000,
|
|
64
66
|
maxTxDelay: 60,
|
|
65
67
|
maxMsbSignedLength: 1_000_000_000,
|
|
68
|
+
maxMsbSignedLengthFutureDelta: 1_000_000,
|
|
66
69
|
maxMsbApplyOperationBytes: 1024 * 1024,
|
|
67
70
|
enableInteractiveMode: true,
|
|
68
71
|
enableBackgroundTasks: false,
|
package/src/msbClient.js
CHANGED
|
@@ -97,11 +97,35 @@ export class MsbClient extends ReadyResource {
|
|
|
97
97
|
return await this.#msb.network.tryConnect(pubKeyHex, role);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
async waitForSignedLengthAtLeast(targetSignedLength) {
|
|
100
|
+
async waitForSignedLengthAtLeast(targetSignedLength, { pollMs = 1_000 } = {}) {
|
|
101
101
|
const core = this.#msb.state?.base?.view?.core ?? null;
|
|
102
102
|
if (!core) throw new Error('MSB view core not available.');
|
|
103
|
+
if (!Number.isSafeInteger(targetSignedLength) || targetSignedLength < 0) {
|
|
104
|
+
throw new Error('Invalid MSB signed length target.');
|
|
105
|
+
}
|
|
106
|
+
if (!Number.isSafeInteger(pollMs) || pollMs < 1) {
|
|
107
|
+
throw new Error('Invalid MSB signed length wait poll interval.');
|
|
108
|
+
}
|
|
103
109
|
while (core.signedLength < targetSignedLength) {
|
|
104
|
-
await new Promise((resolve) =>
|
|
110
|
+
await new Promise((resolve) => {
|
|
111
|
+
const onAppend = () => {
|
|
112
|
+
cleanup();
|
|
113
|
+
resolve();
|
|
114
|
+
};
|
|
115
|
+
const cleanup = () => {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
if (typeof core.off === 'function') {
|
|
118
|
+
core.off('append', onAppend);
|
|
119
|
+
} else if (typeof core.removeListener === 'function') {
|
|
120
|
+
core.removeListener('append', onAppend);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const timer = setTimeout(() => {
|
|
124
|
+
cleanup();
|
|
125
|
+
resolve();
|
|
126
|
+
}, pollMs);
|
|
127
|
+
core.once('append', onAppend);
|
|
128
|
+
});
|
|
105
129
|
}
|
|
106
130
|
}
|
|
107
131
|
|
|
@@ -36,6 +36,11 @@ export class TxOperation {
|
|
|
36
36
|
if(false === this.#validator.validate(op)) return;
|
|
37
37
|
// Stall guard: don't allow a writer to pin apply waiting on an absurd MSB height
|
|
38
38
|
if (op.value.msbsl > this.#config.maxMsbSignedLength) return;
|
|
39
|
+
const localMsbSignedLength = this.#msbClient.getSignedLength();
|
|
40
|
+
if (localMsbSignedLength > 0 &&
|
|
41
|
+
op.value.msbsl > localMsbSignedLength + this.#config.maxMsbSignedLengthFutureDelta) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
39
44
|
// Wait for local MSB view to reach the referenced signed length
|
|
40
45
|
await this.#msbClient.waitForSignedLengthAtLeast(op.value.msbsl);
|
|
41
46
|
// Fetch MSB apply-op at msbsl by tx key (op.key = tx hash)
|
|
@@ -164,6 +164,65 @@ test('apply: tx msbsl stall guard skips waiting', async (t) => {
|
|
|
164
164
|
});
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
+
test('apply: tx msbsl relative future guard skips waiting', async (t) => {
|
|
168
|
+
await withTempDir(async ({ storesDirectory }) => {
|
|
169
|
+
const msbBootstrapBuf = b4a.alloc(32).fill(7);
|
|
170
|
+
const msb = makeMsbStub({
|
|
171
|
+
msbBootstrapBuf,
|
|
172
|
+
signedLength: 100,
|
|
173
|
+
async getEntry() {
|
|
174
|
+
return null;
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
msb.state.base.view.core.once = () => {
|
|
179
|
+
throw new Error('apply should not wait for a far-future msbsl');
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const storeName = 'peer-relative-stall-guard';
|
|
183
|
+
const wallet = await prepareWallet(storesDirectory, storeName);
|
|
184
|
+
const config = createConfig(ENV.DEVELOPMENT, {
|
|
185
|
+
storesDirectory,
|
|
186
|
+
storeName,
|
|
187
|
+
maxMsbSignedLength: 1_000_000_000,
|
|
188
|
+
maxMsbSignedLengthFutureDelta: 10,
|
|
189
|
+
});
|
|
190
|
+
const peer = new Peer({
|
|
191
|
+
config,
|
|
192
|
+
msb,
|
|
193
|
+
protocol: TestProtocol,
|
|
194
|
+
contract: TestContract,
|
|
195
|
+
wallet,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await peer.ready();
|
|
200
|
+
|
|
201
|
+
const txHashHex = makeHex32(1);
|
|
202
|
+
const op = {
|
|
203
|
+
type: 'tx',
|
|
204
|
+
key: txHashHex,
|
|
205
|
+
value: {
|
|
206
|
+
dispatch: { type: 'ping', value: { msg: 'hi' } },
|
|
207
|
+
msbsl: 111,
|
|
208
|
+
ipk: makeHex32(2),
|
|
209
|
+
wp: makeHex32(3),
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const timeout = new Promise((_, reject) =>
|
|
214
|
+
setTimeout(() => reject(new Error('append timed out (possible apply stall)')), 2000)
|
|
215
|
+
);
|
|
216
|
+
await Promise.race([peer.base.append(op), timeout]);
|
|
217
|
+
|
|
218
|
+
const txl = await peer.bee.get('txl');
|
|
219
|
+
t.is(txl, null, 'tx should not be indexed when msbsl exceeds the local future window');
|
|
220
|
+
} finally {
|
|
221
|
+
await closePeer(peer);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
167
226
|
test('apply: tx MSB payload size guard blocks otherwise-valid tx', async (t) => {
|
|
168
227
|
await withTempDir(async ({ storesDirectory }) => {
|
|
169
228
|
const msbBootstrapBuf = b4a.alloc(32).fill(7);
|
|
@@ -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');
|