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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "trac-peer",
3
3
  "main": "src/index.js",
4
- "version": "0.4.7",
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;
@@ -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) => core.once('append', 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
+ });
@@ -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');