@ethernity-cloud/sdk-js 1.7.15 → 1.7.17

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 CHANGED
@@ -44,9 +44,9 @@ The sdk has been tested on the following operating systems:
44
44
  `ecld-publish` registers the image in the ImageRegistry
45
45
  (ECImageRegistryV3, `0xa372a6e1...`) before anything discloses its
46
46
  name, which makes the name the publishing wallet's; then registers the
47
- securelock session on-chain in the SessionRegistry (`0x44442968...`, see
47
+ securelock session on-chain in the SessionRegistry (`0x78f0c5E9...`, see
48
48
  `cas/config.js`), which takes it only from that wallet, resolves a CAS
49
- from the ValidatorRegistry (`0x0014aB10...`, or `ECLD_CAS_ADDR=host:port`)
49
+ from the ValidatorRegistry (`0x8CC8B1dc...`, or `ECLD_CAS_ADDR=host:port`)
50
50
  for the compose and the certificate harvest, takes the trustedzone
51
51
  session name from the ImageRegistry, and links the session to the
52
52
  published image. `ecld-build` signs the securelock `--production`.
@@ -72,6 +72,7 @@ function configure({ identityPriv, swiftStreamService, bucket, contractAddress,
72
72
  _provider = provider;
73
73
  _esrLedger = [];
74
74
  _taskNonces = {};
75
+ _taskWrites = {};
75
76
  _taskRelayNonces = {};
76
77
  _taskLedger = {};
77
78
  _taskCaller = normAddr(caller);
@@ -116,6 +117,13 @@ class StateNonceError extends Error {}
116
117
  // task code 38 (ESR_COMMIT_LIMIT_EXCEEDED).
117
118
  const MAX_COMMITS_PER_TASK = 256;
118
119
 
120
+ // How long a read waits for the node to stage a state object the chain names
121
+ // before the task fails, and how often it looks for it: the node writes the
122
+ // objects pinned on it within seconds of the order's start and the ones it has
123
+ // to fetch from the network as they arrive.
124
+ const STATE_STAGE_WAIT_MS = 120000;
125
+ const STATE_STAGE_POLL_MS = 2000;
126
+
119
127
  class StateLimitError extends Error {}
120
128
 
121
129
  function unwrapStored(stored) {
@@ -260,15 +268,86 @@ function stateKey() {
260
268
  return keccak256(Buffer.concat([Buffer.from(DOMAIN_SEP_STATE, 'utf8'), material]));
261
269
  }
262
270
 
271
+ // A state object is stored as the node's Kubo stores it with `add
272
+ // --cid-version=1 --raw-leaves` and its default chunker: in raw sha256 blocks
273
+ // of BLOCK_SIZE bytes (the last one shorter), a single block as it is, several
274
+ // under a balanced UnixFS tree of at most LINKS_PER_NODE blocks per node.
275
+ // unixfsCid() computes the resulting CID from the bytes, so the enclave
276
+ // commits a CID it derived itself rather than one a node handed it, and the
277
+ // node stores the object under that CID (mvp-pox-node utils.py unixfs_cid).
278
+ const BLOCK_SIZE = 262144;
279
+ const LINKS_PER_NODE = 174;
280
+
281
+ function cidB32(cid) {
282
+ return 'b' + base32Encode(cid).toLowerCase().replace(/=+$/, '');
283
+ }
284
+
285
+ function pbVarint(n) {
286
+ const out = [];
287
+ for (;;) {
288
+ const byte = n % 128;
289
+ n = Math.floor(n / 128);
290
+ if (n === 0) {
291
+ out.push(byte);
292
+ return Buffer.from(out);
293
+ }
294
+ out.push(byte | 0x80);
295
+ }
296
+ }
297
+
298
+ // A protobuf field: varint (wire 0) or length-delimited (wire 2).
299
+ function pbField(number, wire, payload) {
300
+ const tag = pbVarint(number * 8 + wire);
301
+ return wire === 2 ? Buffer.concat([tag, pbVarint(payload.length), payload]) : Buffer.concat([tag, payload]);
302
+ }
303
+
304
+ // CIDv1, codec raw (0x55), sha2-256 (0x12, 32 bytes).
305
+ function pbRawCid(content) {
306
+ return Buffer.concat([Buffer.from([0x01, 0x55, 0x12, 0x20]), crypto.createHash('sha256').update(content).digest()]);
307
+ }
308
+
309
+ // A dag-pb UnixFS File node over `children`, each [cid, tsize, filesize];
310
+ // returns [cid, tsize, filesize]. Links come before Data in dag-pb; a link
311
+ // carries the child's CID, an empty Name and its Tsize, the size of the
312
+ // child's subtree; Data is UnixFS Type=File(2), filesize, blocksizes.
313
+ function pbFileNode(children) {
314
+ const links = Buffer.concat(children.map(([cid, tsize]) =>
315
+ pbField(2, 2, Buffer.concat([pbField(1, 2, cid), pbField(2, 2, Buffer.alloc(0)), pbField(3, 0, pbVarint(tsize))]))));
316
+ const filesize = children.reduce((n, [, , size]) => n + size, 0);
317
+ const data = Buffer.concat([pbField(1, 0, pbVarint(2)), pbField(3, 0, pbVarint(filesize)),
318
+ ...children.map(([, , size]) => pbField(4, 0, pbVarint(size)))]);
319
+ const block = Buffer.concat([links, pbField(1, 2, data)]);
320
+ const cid = Buffer.concat([Buffer.from([0x01, 0x70, 0x12, 0x20]), crypto.createHash('sha256').update(block).digest()]);
321
+ return [cid, block.length + children.reduce((n, [, tsize]) => n + tsize, 0), filesize];
322
+ }
323
+
263
324
  /**
264
- * CIDv1/raw/sha2-256 for `content`, computed without touching IPFS.
265
- * Matches `ipfs add --cid-version=1 --raw-leaves`, which is what lets the
266
- * enclave commit a CID it derived itself rather than one a node handed it.
325
+ * The CID the node's Kubo gives `content`: the raw block's CID up to one
326
+ * block, else the root of the balanced UnixFS tree over the blocks,
327
+ * LINKS_PER_NODE to a node, every node full but the last.
328
+ */
329
+ function unixfsCid(content, blockSize = BLOCK_SIZE) {
330
+ const buf = Buffer.isBuffer(content) ? content : Buffer.from(content);
331
+ if (buf.length <= blockSize) return cidB32(pbRawCid(buf));
332
+ let nodes = [];
333
+ for (let i = 0; i < buf.length; i += blockSize) {
334
+ const chunk = buf.subarray(i, i + blockSize);
335
+ nodes.push([pbRawCid(chunk), chunk.length, chunk.length]);
336
+ }
337
+ while (nodes.length > 1) {
338
+ const next = [];
339
+ for (let i = 0; i < nodes.length; i += LINKS_PER_NODE) next.push(pbFileNode(nodes.slice(i, i + LINKS_PER_NODE)));
340
+ nodes = next;
341
+ }
342
+ return cidB32(nodes[0][0]);
343
+ }
344
+
345
+ /**
346
+ * CIDv1/raw/sha2-256 for `content`: its CID as one raw block, which is its CID
347
+ * through the node's Kubo up to BLOCK_SIZE bytes.
267
348
  */
268
349
  function cidv1Raw(content) {
269
- const digest = crypto.createHash('sha256').update(content).digest();
270
- const raw = Buffer.concat([Buffer.from([0x01, 0x55, 0x12, 0x20]), digest]);
271
- return 'b' + base32Encode(raw).toLowerCase().replace(/=+$/, '');
350
+ return cidB32(pbRawCid(content));
272
351
  }
273
352
 
274
353
  // RFC4648 base32, no padding stripped here (the caller strips it).
@@ -356,6 +435,12 @@ let _esrLedger = [];
356
435
  // even before the node's relay lands on-chain.
357
436
  let _taskNonces = {};
358
437
 
438
+ // Per-task memo of this task's last commit per key, [acl, data, version, cid]
439
+ // as the chain shows them once the node's relay lands. A read prefers it while
440
+ // the chain still shows an older version, so get() after commit() returns the
441
+ // committed state and a second commit to the key builds on the first.
442
+ let _taskWrites = {};
443
+
359
444
  // Per-task memo of the NEXT relay nonce per key hash, so multiple commits to
360
445
  // the same key within one task sign sequential relay nonces even while the
361
446
  // node is still relaying the earlier ones.
@@ -429,6 +514,10 @@ class StateRegistry {
429
514
  /** [acl, data, version, cid] for `key`; NO permission check here. */
430
515
  async _readContainer(key, fallback = {}) {
431
516
  const [cid, version] = await this._contract().getState(this.walletAddress, keyHash(key));
517
+ const own = _taskWrites[key];
518
+ if (own && own[2] > Number(version || 0)) {
519
+ return own;
520
+ }
432
521
  if (!version || !cid) {
433
522
  return [null, fallback, 0, null];
434
523
  }
@@ -579,7 +668,7 @@ class StateRegistry {
579
668
 
580
669
  const blob = encrypt(Buffer.from(JSON.stringify(stored), 'utf8'));
581
670
  // Compute the CID from OUR bytes — never accept one from the node.
582
- const cid = cidv1Raw(blob);
671
+ const cid = unixfsCid(blob);
583
672
 
584
673
  // Hand the blob and the CID to the node for pinning. It verifies the CID
585
674
  // matches; it does not get to choose it. Fire-and-forget.
@@ -597,6 +686,7 @@ class StateRegistry {
597
686
  // results): this is what the chain shows once the relay lands.
598
687
  ledgerRecord(key, currentVersion + 1, cid, newData);
599
688
  _taskNonces[key] = finalNonce;
689
+ _taskWrites[key] = [nextAcl, newData, currentVersion + 1, cid];
600
690
  return [nextAcl, newData];
601
691
  } catch (e) {
602
692
  const msg = String(e && e.message ? e.message : e);
@@ -623,22 +713,40 @@ class StateRegistry {
623
713
  * the node stages state objects in the bucket. That makes the node the
624
714
  * delivery path but not a trusted one: content is checked against the CID the
625
715
  * chain records, so a substituted blob is rejected rather than decrypted.
716
+ *
717
+ * The node stages it as state.<cid>.enc, within seconds of the order's start
718
+ * when the object is pinned on it and as it arrives when it has to fetch it
719
+ * from the network, so the object is looked for every STATE_STAGE_POLL_MS
720
+ * for up to STATE_STAGE_WAIT_MS. state.<key>.enc is the last blob a commit
721
+ * of this key published, which matches only while no later commit was
722
+ * published; one that does not match is passed over.
626
723
  */
627
724
  async _fetch(key, cid) {
628
- for (const name of [`state.${key}.enc`, `state.${cid}.enc`]) {
629
- // eslint-disable-next-line no-await-in-loop
630
- const data = await _swift.getFileContentBytes(_bucket, name).catch(() => null);
631
- if (data && data.length) {
632
- const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
633
- if (cidv1Raw(buf) !== cid) {
634
- throw new Error(
635
- `State object ${name} does not match the committed CID ${cid} — refusing to use it`
636
- );
725
+ const deadline = Date.now() + STATE_STAGE_WAIT_MS;
726
+ let mismatched = [];
727
+ for (;;) {
728
+ mismatched = [];
729
+ for (const name of [`state.${cid}.enc`, `state.${key}.enc`]) {
730
+ // eslint-disable-next-line no-await-in-loop
731
+ const data = await _swift.getFileContentBytes(_bucket, name).catch(() => null);
732
+ if (data && data.length) {
733
+ const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
734
+ if (unixfsCid(buf) === cid) {
735
+ return buf;
736
+ }
737
+ mismatched.push(name);
637
738
  }
638
- return buf;
639
739
  }
740
+ if (Date.now() >= deadline) break;
741
+ // eslint-disable-next-line no-await-in-loop
742
+ await new Promise((resolve) => { setTimeout(resolve, STATE_STAGE_POLL_MS); });
743
+ }
744
+ if (mismatched.length) {
745
+ throw new Error(
746
+ `State object ${mismatched.join(' and ')} does not match the committed CID ${cid} — refusing to use it`
747
+ );
640
748
  }
641
- throw new Error(`Could not read state object for '${key}' (${cid})`);
749
+ throw new Error(`Could not read state object for '${key}' (${cid}): not staged within ${STATE_STAGE_WAIT_MS / 1000} s`);
642
750
  }
643
751
 
644
752
  async _sendCommit(keyHashHex, cid, expectedVersion, ownerAddr = null, nonce = 0) {
@@ -815,6 +923,7 @@ module.exports = {
815
923
  StatePermissionError,
816
924
  StateNonceError,
817
925
  StateLimitError,
926
+ unixfsCid,
818
927
  esrGrant,
819
928
  esrRevoke,
820
929
  esrSetPublicRead,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethernity-cloud/sdk-js",
3
- "version": "1.7.15",
3
+ "version": "1.7.17",
4
4
  "description": "A package to run ethernity cloud build scripts",
5
5
  "scripts": {
6
6
  "test": "node --test \"test/**/*.test.js\"",
@@ -40,7 +40,7 @@
40
40
  "!**/*.pyc"
41
41
  ],
42
42
  "dependencies": {
43
- "@ethernity-cloud/runner": "^0.6.11",
43
+ "@ethernity-cloud/runner": "^0.6.12",
44
44
  "acorn": "^8.11.0",
45
45
  "adm-zip": "^0.5.14",
46
46
  "axios": "^1.7.2",
@@ -46,6 +46,7 @@ import hashlib
46
46
  import io
47
47
  import json
48
48
  import os
49
+ import time
49
50
 
50
51
  DOMAIN_SEP_STATE = b"ethernity-cloud/esr-encryption/v1"
51
52
 
@@ -75,6 +76,11 @@ _task_ledger = {}
75
76
  # get_nonce() right after a commit in the SAME task returns the fresh value
76
77
  # even though the node's relay (and thus the chain) has not landed yet.
77
78
  _task_nonces = {}
79
+ # Per-task memo of this task's last commit per key, (acl, data, version, cid)
80
+ # as the chain shows them once the node's relay lands. A read prefers it while
81
+ # the chain still shows an older version, so get() after commit() returns the
82
+ # committed state and a second commit to the key builds on the first.
83
+ _task_writes = {}
78
84
  # Per-task memo of the NEXT relay nonce per key hash (hex), so multiple
79
85
  # commits to the same key within one task sign sequential relay nonces even
80
86
  # while the node is still relaying the earlier ones.
@@ -312,7 +318,7 @@ def configure(identity_priv, swift_stream_service, bucket, contract_address,
312
318
  value from the client payload or the node's .env.
313
319
  """
314
320
  global _identity_priv, _swift, _bucket, _contract_address, _web3, _esr_evidence
315
- global _task_ledger, _task_nonces, _task_relay_nonces, _task_caller
321
+ global _task_ledger, _task_nonces, _task_writes, _task_relay_nonces, _task_caller
316
322
  _identity_priv = identity_priv
317
323
  _swift = swift_stream_service
318
324
  _bucket = bucket
@@ -320,6 +326,7 @@ def configure(identity_priv, swift_stream_service, bucket, contract_address,
320
326
  _web3 = web3
321
327
  _task_ledger = {}
322
328
  _task_nonces = {}
329
+ _task_writes = {}
323
330
  _task_relay_nonces = {}
324
331
  _esr_evidence = []
325
332
  _task_caller = _norm_addr(caller)
@@ -357,6 +364,13 @@ class StatePermissionError(RuntimeError):
357
364
  # 0x00 = 1 commit ... 0xFF = 256; an all-zero root means no commits at all).
358
365
  MAX_COMMITS_PER_TASK = 256
359
366
 
367
+ # How long a read waits for the node to stage a state object the chain names
368
+ # before the task fails, and how often it looks for it: the node writes the
369
+ # objects pinned on it within seconds of the order's start and the ones it
370
+ # has to fetch from the network as they arrive.
371
+ STATE_STAGE_WAIT_SECONDS = 120
372
+ STATE_STAGE_POLL_SECONDS = 2
373
+
360
374
 
361
375
  class StateLimitError(RuntimeError):
362
376
  """Raised when a task tries to make more than MAX_COMMITS_PER_TASK state
@@ -478,15 +492,77 @@ def looks_like_cid(value) -> bool:
478
492
  return False
479
493
 
480
494
 
481
- def cidv1_raw(content: bytes) -> str:
482
- """CIDv1/raw/sha2-256 for `content`, computed without touching IPFS.
495
+ # A state object is stored as the node's Kubo stores it with `add
496
+ # --cid-version=1 --raw-leaves` and its default chunker: in raw sha256 blocks
497
+ # of BLOCK_SIZE bytes (the last one shorter), a single block as it is, several
498
+ # under a balanced UnixFS tree of at most LINKS_PER_NODE blocks per node.
499
+ # unixfs_cid() computes the resulting CID from the bytes, so the enclave
500
+ # commits a CID it derived itself rather than one a node handed it, and the
501
+ # node stores the object under that CID (mvp-pox-node utils.py unixfs_cid).
502
+ BLOCK_SIZE = 262144
503
+ LINKS_PER_NODE = 174
483
504
 
484
- Matches `ipfs add --cid-version=1 --raw-leaves`, which is what lets the
485
- enclave commit a CID it derived itself rather than one a node handed it.
486
- """
487
- digest = hashlib.sha256(content).digest()
488
- raw = bytes([0x01, 0x55, 0x12, 0x20]) + digest
489
- return "b" + base64.b32encode(raw).decode("ascii").lower().rstrip("=")
505
+
506
+ def _cid_b32(cid: bytes) -> str:
507
+ return "b" + base64.b32encode(cid).decode("ascii").lower().rstrip("=")
508
+
509
+
510
+ def _pb_varint(n: int) -> bytes:
511
+ out = bytearray()
512
+ while True:
513
+ byte = n & 0x7F
514
+ n >>= 7
515
+ if n:
516
+ out.append(byte | 0x80)
517
+ else:
518
+ out.append(byte)
519
+ return bytes(out)
520
+
521
+
522
+ def _pb_field(number: int, wire: int, payload: bytes) -> bytes:
523
+ """A protobuf field: varint (wire 0) or length-delimited (wire 2)."""
524
+ tag = _pb_varint((number << 3) | wire)
525
+ return tag + (_pb_varint(len(payload)) + payload if wire == 2 else payload)
526
+
527
+
528
+ def _pb_raw_cid(content: bytes) -> bytes:
529
+ """CIDv1, codec raw (0x55), sha2-256 (0x12, 32 bytes)."""
530
+ return bytes([0x01, 0x55, 0x12, 0x20]) + hashlib.sha256(content).digest()
531
+
532
+
533
+ def _pb_file_node(children):
534
+ """A dag-pb UnixFS File node over `children`, each (cid, tsize, filesize);
535
+ returns (cid, tsize, filesize). Links come before Data in dag-pb; a link
536
+ carries the child's CID, an empty Name and its Tsize, the size of the
537
+ child's subtree; Data is UnixFS Type=File(2), filesize, blocksizes."""
538
+ links = b"".join(
539
+ _pb_field(2, 2, _pb_field(1, 2, cid) + _pb_field(2, 2, b"") + _pb_field(3, 0, _pb_varint(tsize)))
540
+ for cid, tsize, _ in children)
541
+ filesize = sum(size for _, _, size in children)
542
+ data = _pb_field(1, 0, _pb_varint(2)) + _pb_field(3, 0, _pb_varint(filesize))
543
+ data += b"".join(_pb_field(4, 0, _pb_varint(size)) for _, _, size in children)
544
+ block = links + _pb_field(1, 2, data)
545
+ cid = bytes([0x01, 0x70, 0x12, 0x20]) + hashlib.sha256(block).digest()
546
+ return cid, len(block) + sum(tsize for _, tsize, _ in children), filesize
547
+
548
+
549
+ def unixfs_cid(content: bytes, block_size: int = BLOCK_SIZE) -> str:
550
+ """The CID the node's Kubo gives `content`: the raw block's CID up to one
551
+ block, else the root of the balanced UnixFS tree over the blocks,
552
+ LINKS_PER_NODE to a node, every node full but the last."""
553
+ if len(content) <= block_size:
554
+ return _cid_b32(_pb_raw_cid(content))
555
+ nodes = [(_pb_raw_cid(content[i:i + block_size]), len(content[i:i + block_size]), len(content[i:i + block_size]))
556
+ for i in range(0, len(content), block_size)]
557
+ while len(nodes) > 1:
558
+ nodes = [_pb_file_node(nodes[i:i + LINKS_PER_NODE]) for i in range(0, len(nodes), LINKS_PER_NODE)]
559
+ return _cid_b32(nodes[0][0])
560
+
561
+
562
+ def cidv1_raw(content: bytes) -> str:
563
+ """CIDv1/raw/sha2-256 for `content`: its CID as one raw block, which is
564
+ its CID through the node's Kubo up to BLOCK_SIZE bytes."""
565
+ return _cid_b32(_pb_raw_cid(content))
490
566
 
491
567
 
492
568
  # AES-GCM nonce length, pinned explicitly. PyCryptodome defaults to 16 bytes
@@ -601,6 +677,9 @@ class StateRegistry:
601
677
  contract = self._contract()
602
678
  cid, version, _updated = contract.functions.getState(
603
679
  self.wallet_address, _key_hash(key)).call()
680
+ own = _task_writes.get(key)
681
+ if own is not None and own[2] > int(version or 0):
682
+ return own
604
683
  if not version or not cid:
605
684
  return None, default, 0, None
606
685
  if not looks_like_cid(cid):
@@ -747,7 +826,7 @@ class StateRegistry:
747
826
 
748
827
  blob = _encrypt(json.dumps(stored, separators=(",", ":")).encode("utf-8"))
749
828
  # Compute the CID from OUR bytes -- never accept one from the node.
750
- cid = cidv1_raw(blob)
829
+ cid = unixfs_cid(blob)
751
830
 
752
831
  # Hand the blob and the CID to the node for pinning. It verifies the
753
832
  # CID matches; it does not get to choose it. Fire-and-forget: the
@@ -766,6 +845,7 @@ class StateRegistry:
766
845
  # The ledger records the DATA -- the ACL never leaves in results.
767
846
  _ledger_record(key, current_version + 1, cid, new_data)
768
847
  _task_nonces[key] = final_nonce
848
+ _task_writes[key] = (new_acl, new_data, current_version + 1, cid)
769
849
  return new_acl, new_data
770
850
  except Exception as e:
771
851
  # A version race means someone else committed first: re-read and
@@ -805,17 +885,34 @@ class StateRegistry:
805
885
  delivery path -- but not a trusted one: the content is checked against
806
886
  the CID the chain records, so a substituted or corrupted blob is
807
887
  rejected rather than decrypted and returned as state.
888
+
889
+ The node stages it as state.<cid>.enc, within seconds of the order's
890
+ start when the object is pinned on it and as it arrives when it has
891
+ to fetch it from the network, so the object is looked for every
892
+ STATE_STAGE_POLL_SECONDS for up to STATE_STAGE_WAIT_SECONDS.
893
+ state.<key>.enc is the last blob a commit of this key published,
894
+ which matches only while no later commit was published; one that
895
+ does not match is passed over.
808
896
  """
809
- candidates = (f"state.{key}.enc", f"state.{cid}.enc")
810
- for name in candidates:
811
- ok, data = _swift.get_file_content_bytes(_bucket, name)
812
- if ok and data:
813
- if cidv1_raw(data) != cid:
814
- raise RuntimeError(
815
- f"State object {name} does not match the committed CID "
816
- f"{cid} -- refusing to use it")
817
- return data
818
- raise RuntimeError(f"Could not read state object for '{key}' ({cid})")
897
+ deadline = time.monotonic() + STATE_STAGE_WAIT_SECONDS
898
+ while True:
899
+ mismatched = []
900
+ for name in (f"state.{cid}.enc", f"state.{key}.enc"):
901
+ ok, data = _swift.get_file_content_bytes(_bucket, name)
902
+ if ok and data:
903
+ if unixfs_cid(data) == cid:
904
+ return data
905
+ mismatched.append(name)
906
+ if time.monotonic() >= deadline:
907
+ break
908
+ time.sleep(STATE_STAGE_POLL_SECONDS)
909
+ if mismatched:
910
+ raise RuntimeError(
911
+ f"State object {' and '.join(mismatched)} does not match the "
912
+ f"committed CID {cid} -- refusing to use it")
913
+ raise RuntimeError(
914
+ f"Could not read state object for '{key}' ({cid}): not staged "
915
+ f"within {STATE_STAGE_WAIT_SECONDS} s")
819
916
 
820
917
  def _send_commit(self, key_hash: bytes, cid: str, expected_version: int,
821
918
  owner_addr: str = None, nonce: int = 0):
package/test.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  /* ecld-test — run a Nodenithy payload locally, without SGX, exactly as the
3
3
  * enclave would.
4
4
  *