trac-msb 0.0.9 → 0.0.11

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-msb",
3
3
  "main": "src/index.js",
4
- "version": "0.0.9",
4
+ "version": "0.0.11",
5
5
  "pear": {
6
6
  "name": "trac-msb",
7
7
  "type": "terminal"
@@ -12,11 +12,11 @@
12
12
  "test": "brittle test/*.test.js"
13
13
  },
14
14
  "dependencies": {
15
- "autobase": "7.1.0",
15
+ "autobase": "7.0.45",
16
16
  "b4a": "1.6.7",
17
17
  "bare-fs": "4.0.1",
18
18
  "brittle": "3.0.0",
19
- "corestore": "7.0.23",
19
+ "corestore": "7.0.22",
20
20
  "debounceify": "1.1.0",
21
21
  "trac-wallet": "^0.0.3",
22
22
  "hyperbee": "2.23.0",
@@ -30,5 +30,9 @@
30
30
  "ready-resource": "^1.0.0",
31
31
  "safety-catch": "1.0.2",
32
32
  "xache": "1.2.0"
33
+ },
34
+ "publishConfig": {
35
+ "registry": "https://registry.npmjs.org",
36
+ "access": "public"
33
37
  }
34
38
  }
package/src/functions.js CHANGED
@@ -1,5 +1,4 @@
1
- import BlindPairing from "blind-pairing";
2
-
1
+ import { createHash } from "node:crypto";
3
2
  //TODO: if something is missing, add additonal sanitization
4
3
  // parsed.op === 'pre-tx' -> moved out of the scope this check because we can re-use this function in the apply
5
4
  // TODO: Split sanitization on pre and post TX
@@ -16,11 +15,68 @@ export function sanitizeTransaction(parsedTx) {
16
15
  );
17
16
  }
18
17
 
19
- export async function addWriter(input, peer){
20
- const splitted = input.split(' ');
21
- if(splitted[0] === '/add_writer'){
22
- await peer.base.append({ type: 'addWriter', key: splitted[splitted.length - 1] });
23
- } else if(splitted[0] === '/add_writer2') {
24
- await peer.base.append({ type: 'addWriter2', key: splitted[splitted.length - 1] });
18
+ export function restoreManifest(parsedManifest) {
19
+
20
+ if (Array.isArray(parsedManifest.signers)) {
21
+ parsedManifest.signers = parsedManifest.signers.map(signer => {
22
+ if(signer.namespace && signer.namespace.data &&signer.publicKey && signer.publicKey.data){
23
+ return {
24
+ ...signer,
25
+ namespace: Buffer.from(signer.namespace.data),
26
+ publicKey: Buffer.from(signer.publicKey.data),
27
+ }
28
+ } else {
29
+ return signer;
30
+ }
31
+ });
32
+ }
33
+
34
+ return parsedManifest;
35
+ }
36
+
37
+ // To improve - could be done in better approach (simplify)
38
+ export function restoreHash(parsedPreTx) {
39
+ const reconstructedContentHash = createHash('sha256')
40
+ .update(JSON.stringify(parsedPreTx.ch))
41
+ .digest('hex');
42
+
43
+ const reconstructedTxHash = createHash('sha256')
44
+ .update(
45
+ parsedPreTx.w + '-' +
46
+ parsedPreTx.i + '-' +
47
+ parsedPreTx.ipk + '-' +
48
+ reconstructedContentHash + '-' +
49
+ parsedPreTx.in
50
+ )
51
+ .digest('hex');
52
+
53
+ const finalReconstructedTxHash = createHash('sha256')
54
+ .update(reconstructedTxHash)
55
+ .digest('hex');
56
+ return finalReconstructedTxHash;
57
+ }
58
+
59
+ export async function verifyDag(autoBaseInsance) {
60
+ try {
61
+ console.log('--- DAG Monitoring ---');
62
+ const dagView = await autoBaseInsance.view.core.treeHash();
63
+ const lengthdagView = autoBaseInsance.view.core.length;
64
+ const dagSystem = await autoBaseInsance.system.core.treeHash();
65
+ const lengthdagSystem = autoBaseInsance.system.core.length;
66
+ console.log('this.base.view.core.signedLength:', autoBaseInsance.view.core.signedLength);
67
+ console.log("this.base.signedLength", autoBaseInsance.signedLength);
68
+ console.log("this.base.linearizer.indexers.length", autoBaseInsance.linearizer.indexers.length);
69
+ console.log("this.base.indexedLength", autoBaseInsance.indexedLength);
70
+ console.log(`base.key/writingKey: ${autoBaseInsance.key.toString('hex')}`);
71
+
72
+ console.log(`VIEW Dag: ${dagView.toString('hex')} (length: ${lengthdagView})`);
73
+ console.log(`SYSTEM Dag: ${dagSystem.toString('hex')} (length: ${lengthdagSystem})`);
74
+
75
+ } catch (error) {
76
+ console.error('Error during DAG monitoring:', error.message);
25
77
  }
78
+ }
79
+
80
+ export async function sleep(ms) {
81
+ return new Promise(resolve => setTimeout(resolve, ms));
26
82
  }
package/src/index.js CHANGED
@@ -5,14 +5,20 @@ import ReadyResource from 'ready-resource';
5
5
  import b4a from 'b4a';
6
6
  import Hyperbee from 'hyperbee';
7
7
  import readline from 'readline';
8
- import crypto from 'hypercore-crypto';
9
- import { sanitizeTransaction, addWriter } from './functions.js';
8
+ import { sanitizeTransaction, restoreManifest, sleep, restoreHash ,verifyDag } from './functions.js';
10
9
  import w from 'protomux-wakeup';
11
- import * as edKeyGen from "trac-wallet"
12
- import fs from 'node:fs';
13
10
  import Corestore from 'corestore';
11
+ import verifier from 'hypercore/lib/verifier.js';
12
+ import WriterManager from './writerManager.js';
13
+ import PeerWallet from "trac-wallet"; // TODO: Decide if this should be used here directly or inputed as an option
14
+
15
+
16
+ const { manifestHash, createManifest } = verifier;
14
17
 
15
18
  const wakeup = new w();
19
+ //TODO: change isValid to isIndexer
20
+ //TODO: How about nonce if edDSA is deterministic?
21
+ //TODO: CHECK IF TX HASH IS ALREDY IN BASE BEFORE VALIDATING IT TO DON'T OVERWRITE tx/writerPubKey. Also we need to validate this case where the 2 nodes send the same hash.
16
22
 
17
23
  export class MainSettlementBus extends ReadyResource {
18
24
 
@@ -20,31 +26,35 @@ export class MainSettlementBus extends ReadyResource {
20
26
  super();
21
27
  this.STORES_DIRECTORY = options.stores_directory;
22
28
  this.KEY_PAIR_PATH = `${this.STORES_DIRECTORY}${options.store_name}/db/keypair.json`
23
- this.signingKeyPair = null;
24
29
  this.store = new Corestore(this.STORES_DIRECTORY + options.store_name);
25
30
  this.swarm = null;
26
31
  this.tx = options.tx || null;
27
32
  this.tx_pool = [];
28
33
  this.enable_txchannel = typeof options.enable_txchannel !== "undefined" && options.enable_txchannel === false ? false : true;
29
34
  this.enable_wallet = typeof options.enable_wallet !== "undefined" && options.enable_wallet === false ? false : true;
35
+ this.isVerifyOnly = typeof options.isVerifyOnly !== "undefined" && options.isVerifyOnly === true ? true : false;
30
36
  this.base = null;
31
- this.key = null;
32
37
  this.channel = options.channel || null;
33
38
  this.connectedNodes = 1;
34
39
  this.replicate = options.replicate !== false;
35
- this.writerLocalKey = null;
40
+ this.writingKey = null;
36
41
  this.isStreaming = false;
37
42
  this.bootstrap = options.bootstrap || null;
38
43
  this.opts = options;
39
- this.connectedPeers = new Set();
40
44
  this.bee = null;
41
45
 
46
+ // TODO: Decide if this is better placed in the _open method instead of here
47
+ if (this.enable_wallet) {
48
+ this.wallet = new PeerWallet({ isVerifyOnly: this.isVerifyOnly });
49
+ }
50
+
42
51
  this.pool();
43
52
  this.msbListener();
44
53
  this._boot();
45
54
  this.ready().catch(noop);
46
- }
47
55
 
56
+ }
57
+ //TODO: Move apply to the separate file
48
58
  _boot() {
49
59
  const _this = this;
50
60
  this.base = new Autobase(this.store, this.bootstrap, {
@@ -56,6 +66,8 @@ export class MainSettlementBus extends ReadyResource {
56
66
  keyEncoding: 'utf-8',
57
67
  valueEncoding: 'json'
58
68
  })
69
+ _this.keysView = _this.bee.sub('pubKeys');
70
+
59
71
  return _this.bee;
60
72
  },
61
73
 
@@ -64,25 +76,140 @@ export class MainSettlementBus extends ReadyResource {
64
76
  for (const node of nodes) {
65
77
  const op = node.value;
66
78
  const postTx = op.value;
67
- if (op.type === 'tx') {
79
+
80
+ if (!op || !op.type || !op.key || !op.value) {
81
+ continue;
82
+ }
83
+ // WRITING & INDEXING
84
+ if (op.type === 'addWriter') {
85
+ //TODO: it can be optimalized by adding variables to don't call Buffer.from multiple times.
86
+ //TODO: SANITIZE INCOMPING PROPOSAL
87
+ if (node.from.key.toString('hex') === this.bootstrap) {
88
+ const message = Buffer.concat([
89
+ Buffer.from(JSON.stringify(op.value.hpm)),
90
+ Buffer.from(op.value.wk, 'hex'),
91
+ Buffer.from(op.key, 'hex')
92
+ //TODO: ADD THE NONCE?
93
+ ]);
94
+
95
+ const pop1Valid = this.wallet.verify(op.value.pop1, message, Buffer.from(op.value.hpm.signers[0].publicKey));
96
+ const pop2Valid = this.wallet.verify(op.value.pop2, message, op.key);
97
+
98
+
99
+ const restoredManifest = restoreManifest(op.value.hpm); //temporary workaround
100
+ if (pop1Valid && pop2Valid && manifestHash(createManifest(restoredManifest)).toString('hex') === op.value.wk) {
101
+
102
+ const writerEntry = this.base.view.get(op.key);
103
+ if (writerEntry === null || !writerEntry.isValid) {
104
+ await base.addWriter(b4a.from(op.value.wk, 'hex'), { isIndexer: false })
105
+
106
+ await view.put(op.key, {
107
+ wk: op.value.wk,
108
+ hpm: op.value.hpm,
109
+ pop1: op.value.pop1, // TODO: observation this is really necessary to store pops? IF NOT DELETE IT!
110
+ pop2: op.value.pop2,
111
+ isValid: true,
112
+ isIndexer: false
113
+ //TODO: ADD NONCE?
114
+ });
115
+ console.log(`Writer added: ${op.value.wk}`);
116
+
117
+ }
118
+ }
119
+ }
120
+ } else if (op.type === 'removeWriter') {
121
+ //TODO: it can be optimalized by adding variables to don't call Buffer.from multiple times. And other operations
122
+ //TODO: SANITIZE INCOMPING PROPOSAL
123
+ if (node.from.key.toString('hex') === this.bootstrap) {
124
+ const publicKey = Buffer.from(op.key, 'hex');
125
+ const message = Buffer.concat([
126
+ publicKey
127
+ //TODO: ADD NONCE ?
128
+ ]);
129
+
130
+ const popIsValid = this.wallet.verify(op.value.pop, message, publicKey);
131
+ if (popIsValid) {
132
+ const writerEntry = await _this.base.view.get(op.key)
133
+ if (writerEntry !== null && writerEntry.value.isValid ) {
134
+ await base.removeWriter(Buffer.from(writerEntry.value.wk, 'hex'));
135
+ writerEntry.value.isValid = false;
136
+ if (writerEntry.value.isIndexer === true) {
137
+ writerEntry.value.isIndexer = false;
138
+ }
139
+ await view.put(op.key, writerEntry.value);
140
+ console.log(`Writer removed: ${writerEntry.value.wk}`);
141
+ }
142
+
143
+ }
144
+ }
145
+ } else if (op.type === 'initBootstrap') {
146
+ // this operation initializes the bootstrap skp to grant this public key the owner status.
147
+ //TODO: ADD MORE SANITIZATION. THIS IS STILL JS.
148
+ //TODO: HANDLE ERRORS?
149
+ //TODO: it can be optimalized by adding variables to don't call Buffer.from multiple times.
150
+ if (node.from.key.toString('hex') === this.bootstrap) {
151
+ const message = Buffer.concat([
152
+ Buffer.from(JSON.stringify(op.value.hpm)),
153
+ Buffer.from(op.value.wk, 'hex'),
154
+ Buffer.from(op.value.skp, 'hex')
155
+ //here should be nonce anyway in the future - generated randomly from the huge space.
156
+ ]);
157
+
158
+ const pop1Valid = this.wallet.verify(op.value.pop1, message, Buffer.from(op.value.hpm.signers[0].publicKey));
159
+ const pop2Valid = this.wallet.verify(op.value.pop2, message, Buffer.from(op.value.skp, 'hex'));
160
+ const restoredManifest = restoreManifest(op.value.hpm); //temporary workaround
161
+
162
+ if (pop1Valid && pop2Valid && manifestHash(createManifest(restoredManifest)).toString('hex') === this.bootstrap) {
163
+ await view.put(op.key, op.value);
164
+ }
165
+ }
166
+ } else if (op.type === 'addIndexer') {
167
+ if (node.from.key.toString('hex') === _this.bootstrap) {
168
+ //Simplify signature
169
+ const message = Buffer.concat([
170
+ Buffer.from(op.value.ptpk, 'hex'),
171
+ Buffer.from(op.value.pwk, 'hex')]
172
+ )
173
+ const popValid = _this.wallet.verify(op.value.pop, message, op.key);
174
+ const writerEntry = await _this.base.view.get(op.value.ptpk);
175
+ if (popValid && writerEntry !== null && writerEntry.value.isValid === true && writerEntry.value.wk === op.value.pwk && writerEntry.value.isIndexer === false) {
176
+ await base.removeWriter(Buffer.from(writerEntry.value.wk, 'hex'));
177
+ await base.addWriter(b4a.from(writerEntry.value.wk, 'hex'), { indexer: true })
178
+ writerEntry.value.isIndexer = true;
179
+ await view.put(op.value.ptpk, writerEntry.value);
180
+ console.log(`${op.value.ptpk}:${op.value.pwk} writer became indexer`);
181
+ }
182
+ }
183
+ } else if (op.type === 'removeIndexer') {
184
+ if (node.from.key.toString('hex') === _this.bootstrap) {
185
+ //Simplify signature
186
+ const message = Buffer.concat([
187
+ Buffer.from(op.value.ptpk, 'hex'),
188
+ Buffer.from(op.value.pwk, 'hex')]
189
+ )
190
+ const popValid = _this.wallet.verify(op.value.pop, message, op.key);
191
+ const writerEntry = await _this.base.view.get(op.value.ptpk);
192
+
193
+ if (popValid && writerEntry !== null && writerEntry.value.isValid === true && writerEntry.value.wk === op.value.pwk && writerEntry.value.isIndexer === true) {
194
+ await base.removeWriter(Buffer.from(writerEntry.value.wk, 'hex'));
195
+ await base.addWriter(b4a.from(writerEntry.value.wk, 'hex'), { indexer: false })
196
+ writerEntry.value.isIndexer = false;
197
+ await view.put(op.value.ptpk, writerEntry.value);
198
+ console.log(`Writer ${op.value.ptpk}:${op.value.pwk} is not longer indexer`);
199
+ }
200
+ }
201
+ } else if (op.type === 'tx') {
68
202
  if (null === await view.get(op.key) &&
203
+ restoreHash(postTx) !== postTx.tx &&
69
204
  sanitizeTransaction(postTx) &&
70
205
  postTx.op === 'post-tx' &&
71
- crypto.verify(Buffer.from(postTx.tx, 'utf-8'), Buffer.from(postTx.is, 'hex'), Buffer.from(postTx.ipk, 'hex')) &&// sender verification
72
- crypto.verify(Buffer.from(postTx.tx, 'utf-8'), Buffer.from(postTx.ws, 'hex'), Buffer.from(postTx.wp, 'hex')) &&// writer verification
206
+ this.wallet.verify(Buffer.from(postTx.is, 'hex'), Buffer.from(postTx.tx, 'utf-8'), Buffer.from(postTx.ipk, 'hex')) &&// sender verification
207
+ this.wallet.verify(Buffer.from(postTx.ws, 'hex'), Buffer.from(postTx.tx, 'utf-8'), Buffer.from(postTx.wp, 'hex')) &&// writer verification
73
208
  Buffer.byteLength(JSON.stringify(postTx)) <= 4096
74
209
  ) {
75
210
  await view.put(op.key, op.value);
76
- console.log(`TX: ${op.key} appended. Signed length: `, _this.base.view.core.signedLength);
211
+ console.log(`TX: ${op.key} appended. Signed length: `, _this.base.view.core.signedLength);
77
212
  }
78
- } else if (op.type === 'addWriter') {
79
- const writerKey = b4a.from(op.key, 'hex');
80
- await base.addWriter(writerKey);
81
- console.log(`Writer added: ${op.key}`);
82
- } else if (op.type === 'addWriter2') {
83
- const writerKey = b4a.from(op.key, 'hex');
84
- await base.addWriter(writerKey, { isIndexer : false });
85
- console.log(`Writer added: ${op.key} non-indexer`);
86
213
  }
87
214
  }
88
215
  }
@@ -92,17 +219,23 @@ export class MainSettlementBus extends ReadyResource {
92
219
 
93
220
  async _open() {
94
221
  await this.base.ready();
95
- if(this.enable_wallet){
96
- await this.#initKeyPair();
222
+ if (this.enable_wallet && !this.isVerifyOnly) {
223
+ await this.wallet.initKeyPair(this.KEY_PAIR_PATH);
97
224
  }
98
- console.log('View Length:', this.base.view.core.length);
99
- console.log('View Signed Length:', this.base.view.core.signedLength);
100
- console.log('MSB Key:', Buffer(this.base.view.core.key).toString('hex'));
101
- this.writerLocalKey = b4a.toString(this.base.local.key, 'hex');
225
+
226
+ this.writingKey = b4a.toString(this.base.local.key, 'hex');
227
+
102
228
  if (this.replicate) await this._replicate();
103
229
  if (this.enable_txchannel) {
104
230
  await this.txChannel();
105
231
  }
232
+ this.writerManager = new WriterManager(this);
233
+
234
+ console.log('View Length:', this.base.view.core.length);
235
+ console.log('View Signed Length:', this.base.view.core.signedLength);
236
+ console.log('MSB Key:', Buffer(this.base.view.core.key).toString('hex'));
237
+ console.log(`isWritable? ${this.base.writable}`);
238
+ console.log(`isIndexer: ${this.base.isIndexer}`);
106
239
  }
107
240
 
108
241
  async close() {
@@ -116,23 +249,18 @@ export class MainSettlementBus extends ReadyResource {
116
249
  this.tx_swarm = new Hyperswarm({ maxPeers: 1024, maxParallel: 512, maxServerConnections: 256 });
117
250
  this.tx_swarm.on('connection', async (connection, peerInfo) => {
118
251
  const _this = this;
119
- const peerName = b4a.toString(connection.remotePublicKey, 'hex');
120
- this.connectedPeers.add(peerName);
121
- this.connectedNodes++;
122
252
 
123
253
  connection.on('close', () => {
124
- this.connectedNodes--;
125
- this.connectedPeers.delete(peerName);
126
254
  });
127
255
 
128
256
  connection.on('error', (error) => { });
129
257
 
130
258
  connection.on('data', async (msg) => {
131
259
 
132
- if(_this.base.isIndexer) return;
260
+ if (!_this.base.isIndexer) return;
133
261
 
134
262
  // TODO: decide if a tx rejection should be responded with
135
- if(_this.tx_pool.length >= 1000) {
263
+ if (_this.tx_pool.length >= 1000) {
136
264
  console.log('pool full');
137
265
  return
138
266
  }
@@ -140,16 +268,17 @@ export class MainSettlementBus extends ReadyResource {
140
268
  if(Buffer.byteLength(msg) > 3072) return;
141
269
 
142
270
  try {
143
-
144
271
  const parsedPreTx = JSON.parse(msg);
145
272
 
146
273
  if (sanitizeTransaction(parsedPreTx) &&
147
274
  parsedPreTx.op === 'pre-tx' &&
148
- crypto.verify(Buffer.from(parsedPreTx.tx, 'utf-8'), Buffer.from(parsedPreTx.is, 'hex'), Buffer.from(parsedPreTx.ipk, 'hex')) &&
149
- parsedPreTx.w === _this.writerLocalKey &&
275
+ restoreHash(parsedPreTx) !== parsedPreTx.tx &&
276
+ this.wallet.verify(Buffer.from(parsedPreTx.is, 'hex'), Buffer.from(parsedPreTx.tx, 'utf-8'), Buffer.from(parsedPreTx.ipk, 'hex')) &&
277
+ parsedPreTx.w === _this.writingKey &&
150
278
  null === await _this.base.view.get(parsedPreTx.tx)
151
279
  ) {
152
- const signature = crypto.sign(Buffer.from(parsedPreTx.tx, 'utf-8'), this.signingKeyPair.secretKey);
280
+ const signature = this.wallet.sign(Buffer.from(parsedPreTx.tx, 'utf-8'));
281
+
153
282
  const append_tx = {
154
283
  op: 'post-tx',
155
284
  tx: parsedPreTx.tx,
@@ -159,10 +288,10 @@ export class MainSettlementBus extends ReadyResource {
159
288
  ipk: parsedPreTx.ipk,
160
289
  ch: parsedPreTx.ch,
161
290
  in: parsedPreTx.in,
162
- ws: signature.toString('hex'),
163
- wp: this.signingKeyPair.publicKey.toString('hex'),
291
+ ws: JSON.parse(JSON.stringify(signature)),
292
+ wp: JSON.parse(JSON.stringify(this.wallet.publicKey)),
164
293
  };
165
- _this.tx_pool.push({ tx: parsedPreTx.tx, append_tx : append_tx });
294
+ _this.tx_pool.push({ tx: parsedPreTx.tx, append_tx: append_tx });
166
295
  }
167
296
  } catch (e) {
168
297
  console.log(e)
@@ -176,44 +305,40 @@ export class MainSettlementBus extends ReadyResource {
176
305
  console.log('Joined MSB TX channel');
177
306
  }
178
307
 
179
- async pool(){
180
- while(true){
181
- if(this.tx_pool.length > 0){
308
+ async pool() {
309
+ while (true) {
310
+ if (this.tx_pool.length > 0) {
182
311
  const length = this.tx_pool.length;
183
- for(let i = 0; i < length; i++){
312
+ for (let i = 0; i < length; i++) {
184
313
  await this.base.append({ type: 'tx', key: this.tx_pool[i].tx, value: this.tx_pool[i].append_tx });
185
- await this.sleep(5);
314
+ await sleep(5);
186
315
  }
187
316
  this.tx_pool.splice(0, length);
188
317
  }
189
- await this.sleep(10);
318
+ await sleep(10);
190
319
  }
191
320
  }
192
321
 
193
- async sleep(ms) {
194
- return new Promise(resolve => setTimeout(resolve, ms));
195
- }
196
-
197
322
  async _replicate() {
198
323
  if (!this.swarm) {
199
- const keyPair = await this.store.createKeyPair('hyperswarm');
324
+ const keyPair = this.base.local.keyPair //await this.store.createKeyPair('hyperswarm');
200
325
  this.swarm = new Hyperswarm({ keyPair, maxPeers: 1024, maxParallel: 512, maxServerConnections: 256 });
201
326
 
202
327
  console.log(`Channel: ${this.channel}`);
203
- console.log(`Writer key: ${this.writerLocalKey}`)
204
- console.log(`isIndexer: ${this.base.isIndexer}`);
328
+ console.log(`Writer key: ${this.writingKey}`)
329
+
205
330
  this.swarm.on('connection', async (connection, peerInfo) => {
206
- const peerName = b4a.toString(connection.remotePublicKey, 'hex');
207
- this.connectedPeers.add(peerName);
208
331
  wakeup.addStream(connection);
209
332
  this.store.replicate(connection);
210
- this.connectedNodes++;
333
+
211
334
 
212
335
  connection.on('close', () => {
213
- this.connectedNodes--;
214
- this.connectedPeers.delete(peerName);
215
336
  });
216
337
 
338
+ connection.on('data', async data => {
339
+ await WriterManager.handleIncomingWriterEvent(this, data);
340
+ })
341
+
217
342
  connection.on('error', (error) => { });
218
343
 
219
344
  if (!this.isStreaming) {
@@ -236,30 +361,6 @@ export class MainSettlementBus extends ReadyResource {
236
361
  });
237
362
  }
238
363
 
239
- async verifyDag() {
240
- try {
241
- console.log('--- DAG Monitoring ---');
242
- const dagView = await this.base.view.core.treeHash();
243
- const lengthdagView = this.base.view.core.length;
244
- const dagSystem = await this.base.system.core.treeHash();
245
- const lengthdagSystem = this.base.system.core.length;
246
- console.log('this.base.view.core.signedLength:', this.base.view.core.signedLength);
247
- console.log("this.base.signedLength", this.base.signedLength);
248
- console.log("this.base.linearizer.indexers.length", this.base.linearizer.indexers.length);
249
- console.log("this.base.indexedLength", this.base.indexedLength);
250
- //console.log("this.base.system.core", this.base.system.core);
251
- console.log(`writerLocalKey: ${this.writerLocalKey}`);
252
- console.log(`base.key: ${this.base.key.toString('hex')}`);
253
- console.log('discoveryKey:', b4a.toString(this.base.discoveryKey, 'hex'));
254
-
255
- console.log(`VIEW Dag: ${dagView.toString('hex')} (length: ${lengthdagView})`);
256
- console.log(`SYSTEM Dag: ${dagSystem.toString('hex')} (length: ${lengthdagSystem})`);
257
-
258
- } catch (error) {
259
- console.error('Error during DAG monitoring:', error.message);
260
- }
261
- }
262
-
263
364
  async interactiveMode() {
264
365
  const rl = readline.createInterface({
265
366
  input: process.stdin,
@@ -267,102 +368,43 @@ export class MainSettlementBus extends ReadyResource {
267
368
  });
268
369
 
269
370
  console.log('MSB started. Available commands:');
270
- console.log('- /add_writer: enter a peer writer key as argument to get included as writer.');
271
- console.log('- /add_writer2: enter a peer writer key as argument to get included as non-indexing writer.');
371
+ console.log('- /addMe: send request to admin to become a writer node in the TRAC Network');
372
+ console.log('- /removeMe: send request to admin to remove writer node from the TRAC Network');
373
+ console.log('- /addIndexer <TracPublicKey> <WritingKey> (Admin only): enter a public key and writing key to make node as Indexer. Node have to be a writer already. ');
374
+ console.log('- /removeIndexer <TracPublicKey> <WritingKey> (Admin only): enter a public key and writing key to take off Indexer role. Node have to be a writer already');
272
375
  console.log('- /dag: check system properties such as writer key, DAG, etc.');
273
376
  console.log('- /exit: Exit the program');
274
377
 
275
378
  rl.on('line', async (input) => {
276
379
  switch (input) {
277
380
  case '/dag':
278
- await this.verifyDag();
381
+ await verifyDag(this.base);
279
382
  break;
280
383
  case '/exit':
281
384
  console.log('Exiting...');
282
385
  rl.close();
283
386
  await this.close();
284
387
  process.exit(0);
388
+ case '/addMe':
389
+ await this.writerManager.addMe();
390
+ break;
391
+ case '/removeMe':
392
+ await this.writerManager.removeMe();
285
393
  break;
286
394
  default:
287
- if (input.startsWith('/add_writer')) {
288
- await addWriter(input, this);
395
+ if (input.startsWith('/addIndexer')) {
396
+ const splitted = input.split(' ');
397
+ this.writerManager.addIndexer(splitted[1], splitted[2]);
398
+
399
+ } else if (input.startsWith('/removeIndexer')) {
400
+ const splitted = input.split(' ');
401
+ this.writerManager.removeIndexer(splitted[1], splitted[2]);
289
402
  }
290
403
  }
291
404
  rl.prompt();
292
405
  });
293
-
294
406
  rl.prompt();
295
407
  }
296
-
297
- async #getMnemonicInteractiveMode() {
298
- const rl = readline.createInterface({
299
- input: process.stdin,
300
- output: process.stdout
301
- });
302
-
303
- const question = (query) => {
304
- return new Promise(resolve => {
305
- rl.question(query, resolve);
306
- });
307
- }
308
-
309
- let mnemonic;
310
- let choice = '';
311
- while (!choice.trim()) {
312
- choice = await question("[1]. Generate new mnemonic phrase\n[2]. Restore keypair from backed up mnemonic phrase\nYour choice (1/2): ");
313
- switch (choice) {
314
- case '1':
315
- mnemonic = undefined
316
- break;
317
- case '2':
318
- const mnemonicInput = await question("Enter your mnemonic phrase: ");
319
- mnemonic = edKeyGen.sanitizeMnemonic(mnemonicInput);
320
- break;
321
- default:
322
- console.log("Invalid choice. Please select again");
323
- choice = '';
324
- break;
325
- }
326
- }
327
- rl.close();
328
- return mnemonic;
329
- }
330
-
331
- async #initKeyPair() {
332
- // TODO: User shouldn't be allowed to store it in unencrypted form. ASK for a password to encrypt it. ENCRYPT(HASH(PASSWORD,SALT),FILE)/DECRYPT(HASH(PASSWORD,SALT),ENCRYPTED_FILE)?
333
- try {
334
- // Check if the key file exists
335
- if (fs.existsSync(this.KEY_PAIR_PATH)) {
336
- const keyPair = JSON.parse(fs.readFileSync(this.KEY_PAIR_PATH));
337
- this.signingKeyPair = {
338
- publicKey: Buffer.from(keyPair.publicKey, 'hex'),
339
- secretKey: Buffer.from(keyPair.secretKey, 'hex')
340
- }
341
- } else {
342
- console.log("Key file was not found. How do you wish to proceed?");
343
- const mnemonic = await this.#getMnemonicInteractiveMode();
344
-
345
- const generatedSecrets = edKeyGen.generateKeyPair(mnemonic);
346
- const keyPair = {
347
- publicKey: Buffer.from(generatedSecrets.publicKey).toString('hex'),
348
- secretKey: Buffer.from(generatedSecrets.secretKey).toString('hex')
349
- }
350
-
351
- //TODO: ASK USER TO WRITE FIRST SECOND AND LAST WORD OR SOMETHING SIMILAR TO CONFIRM THEY HAVE WRITTEN IT DOWN
352
- if (!mnemonic) console.log("This is your mnemonic:\n", generatedSecrets.mnemonic, "\nPlease back it up in a safe location")
353
-
354
- fs.writeFileSync(this.KEY_PAIR_PATH, JSON.stringify(keyPair));
355
- this.signingKeyPair = {
356
- publicKey: generatedSecrets.publicKey,
357
- secretKey: generatedSecrets.secretKey,
358
- }
359
-
360
- console.log("DEBUG: Key pair generated and stored in", this.KEY_PAIR_PATH);
361
- }
362
- } catch (err) {
363
- console.error(err);
364
- }
365
- }
366
408
  }
367
409
 
368
410
  function noop() { }
@@ -0,0 +1,316 @@
1
+ import ReadyResource from 'ready-resource';
2
+
3
+ //TODO ADD THROWS
4
+ //TODO FOR NOW IT'S FINE. IT MUST BE INTEGRATED WITH TRAC WALLET
5
+ //TODO: How about nonce if edDSA is deterministic?
6
+ //TODO: if enable_wallet is false then user shouldn't be allowd to add or remove writer
7
+ /**
8
+ * WriterManager manages writer nodes in the TRAC NETWORK, handling events, adding/removing writers, and managing the bootstrap process.
9
+ * It interacts with the MainSettlementBus instance to perform key operations.
10
+ */
11
+
12
+ const MS_TO_WAIT = 5000;
13
+
14
+ export class WriterManager extends ReadyResource {
15
+ /**
16
+ * Initializes the WriterManager with the given MainSettlementBus instance.
17
+ * @param {MainSettlementBus} msbInstance - An instance of the MainSettlementBus class.
18
+ */
19
+ constructor(msbInstance) {
20
+ super();
21
+ this.base = msbInstance.base;
22
+ this.wallet = msbInstance.wallet;
23
+ this.swarm = msbInstance.swarm;
24
+ this.bootstrap = msbInstance.bootstrap;
25
+ this.writingKey = msbInstance.writingKey;
26
+
27
+ this.#initBootstrap(msbInstance);
28
+
29
+ }
30
+
31
+ /**
32
+ * Initializes the bootstrap initialization process if the node is a bootstrap node and can be only executed by the bootstrap node.
33
+ * @returns {Promise<void>} Resolves once the bootstrap initialization is complete.
34
+ */
35
+ async #initBootstrap(msbInstance) {
36
+ try {
37
+
38
+ if (this.writingKey && this.writingKey === this.bootstrap) {
39
+
40
+ const bootStrapEntry = await this.getBootstrapEntry();
41
+ if (bootStrapEntry === null) {
42
+
43
+ const message = Buffer.concat([
44
+ Buffer.from(JSON.stringify(this.base.localWriter.core.manifest)),
45
+ Buffer.from(this.writingKey, 'hex'),
46
+ Buffer.from(this.wallet.publicKey, 'hex')
47
+ ]);
48
+
49
+ //SEND BOOTSTRAP TRAC MANIFEST
50
+ await this.base.append({
51
+ type: 'initBootstrap',
52
+ key: 'bootstrap',
53
+ value: {
54
+ wk: this.writingKey,
55
+ hpm: this.base.localWriter.core.manifest,
56
+ skp: this.wallet.publicKey,
57
+ pop1: this.wallet.sign(message, this.base.localWriter.core.keyPair.secretKey),
58
+ pop2: this.wallet.sign(message)
59
+ }
60
+ });
61
+ }
62
+ console.log(`Bootstrap node ${this.writingKey} is ready to accept writers`);
63
+ await this.#WriterEventListener(msbInstance);
64
+ }
65
+ } catch (error) {
66
+ console.error(`err in `, error);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Listens for writer events and processes them by appending the parsed request to the base.
72
+ * At this moment only Bootstrap is listening for writer events.
73
+ * @private
74
+ */
75
+ async #WriterEventListener(msbInstance) {
76
+ msbInstance.on('writerEvent', async (parsedRequest) => {
77
+ await this.base.append(parsedRequest);
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Adds the current node as a writer if it is not already a writer.
83
+ * The node must connect to the bootstrap node to complete the operation.
84
+ * @returns {Promise<void>} Resolves once the writer has been added or if the operation fails.
85
+ */
86
+ async addMe() {
87
+ try {
88
+
89
+ if (this.writingKey === this.bootstrap) {
90
+ console.log('Bootstrap node cannot add itself');
91
+ return;
92
+ }
93
+
94
+ const writerEntry = await this.base.view.get(this.wallet.publicKey);
95
+ if (writerEntry !== null && writerEntry.value.isValid) {
96
+ console.log(`Cannot perform operation because node is already writer`);
97
+ return;
98
+ }
99
+
100
+ const bootstrapEntry = await this.getBootstrapEntry();
101
+ if (!bootstrapEntry?.value?.hpm?.signers?.[0]?.publicKey?.data) {
102
+ console.log(`Bootstrap key not found`);
103
+ return;
104
+ }
105
+
106
+ const bootstrapPubKey = Buffer.from((bootstrapEntry.value.hpm.signers[0].publicKey.data)).toString('hex');
107
+
108
+ this.swarm.connections.forEach(async conn => {
109
+ if (conn.connected && conn.remotePublicKey.toString('hex') === bootstrapPubKey) {
110
+
111
+ const message = Buffer.concat([
112
+ Buffer.from(JSON.stringify(this.base.local.core.manifest)),
113
+ Buffer.from(this.writingKey, 'hex'),
114
+ Buffer.from(this.wallet.publicKey, 'hex')
115
+ //TODO: ADD NONCE?
116
+ ]);
117
+
118
+ //SEND TRAC MANIFEST
119
+ conn.write(JSON.stringify({
120
+ type: 'addWriter',
121
+ key: this.wallet.publicKey,
122
+ value: {
123
+ wk: this.writingKey,
124
+ hpm: this.base.local.core.manifest,
125
+ pop1: this.wallet.sign(message, this.base.local.core.header.keyPair.secretKey),
126
+ pop2: this.wallet.sign(message),
127
+ //TODO: ADD NONCE?
128
+ }
129
+
130
+ }));
131
+
132
+ setTimeout(async () => {
133
+ const updatedWriterEntry = await this.base.view.get(this.wallet.publicKey);
134
+ if (updatedWriterEntry !== null && updatedWriterEntry.value.isValid) {
135
+ console.log(`Writer ${this.writingKey} was successfully added.`);
136
+ } else {
137
+ console.warn(`Writer ${this.writingKey} was NOT added.`);
138
+ }
139
+ }, MS_TO_WAIT);
140
+ }
141
+ })
142
+ } catch (error) {
143
+ console.error(`err in `, error);
144
+ }
145
+ }
146
+ /**
147
+ * Removes the current node as a writer.
148
+ * The node must connect to the bootstrap node to complete the removal operation.
149
+ * @returns {Promise<void>} Resolves once the writer has been removed or if the operation fails.
150
+ */
151
+ async removeMe() {
152
+ try {
153
+
154
+ if (this.writingKey === this.bootstrap) {
155
+ console.log('Bootstrap node cannot remove itself');
156
+ return;
157
+ }
158
+
159
+ const writerEntry = await this.base.view.get(this.wallet.publicKey);
160
+ if (writerEntry === null || !writerEntry.value.isValid) {
161
+ console.log(`Your key does not exist in the database or you can't remove it`);
162
+ return;
163
+ }
164
+
165
+ const bootstrapEntry = await this.getBootstrapEntry();
166
+
167
+ if (bootstrapEntry === null || !bootstrapEntry.value?.hpm?.signers?.[0]?.publicKey?.data) {
168
+ console.log(`Bootstrap key not found`);
169
+ return;
170
+ }
171
+
172
+ const bootstrapPubKey = Buffer.from(bootstrapEntry.value.hpm.signers[0].publicKey.data).toString('hex');
173
+
174
+ this.swarm.connections.forEach(async conn => {
175
+ if (conn.connected && conn.remotePublicKey.toString('hex') === bootstrapPubKey) {
176
+
177
+ const message = Buffer.concat([
178
+ Buffer.from(this.wallet.publicKey, 'hex') // TODO: TO REDUCE THE SIZE OF THE MESSAGE WE CAN SEND SIMPLE STRING SUCHAS "REMOVE". You can just HASH the message and sign the hash.
179
+ //TODO: ADD NONCE?
180
+ ]);
181
+
182
+ //SEND TRAC MANIFEST
183
+ conn.write(JSON.stringify({
184
+ type: 'removeWriter',
185
+ key: this.wallet.publicKey,
186
+ value: {
187
+ pop: this.wallet.sign(message),
188
+ //TODO: ADD NONCE?
189
+ }
190
+ }));
191
+ }
192
+ })
193
+
194
+ setTimeout(async () => {
195
+ const updatedWriterEntry = await this.base.view.get(this.wallet.publicKey);
196
+ if (updatedWriterEntry !== null && !updatedWriterEntry.value.isValid) {
197
+ console.log(`Key successfully removed`);
198
+ } else {
199
+ console.log(`Failed to remove key`);
200
+ }
201
+ }, MS_TO_WAIT);
202
+
203
+ } catch (error) {
204
+ console.error(`err in `, error);
205
+ }
206
+ }
207
+
208
+
209
+ /**
210
+ * Adds a writer node as an indexer.
211
+ * The node must already be a valid writer to become an indexer.
212
+ * This operation can only be performed by the bootstrap/admin node.
213
+ *
214
+ * @param {string} peerTracPublicKey - The TRAC public key of the writer node to be promoted to indexer.
215
+ * @param {string} peerWritingKey - The writing key of the writer node to be promoted to indexer.
216
+ * @returns {Promise<void>} Resolves once the writer has been promoted to indexer or if the operation fails.
217
+ */
218
+ async addIndexer(peerTracPublicKey, peerWritingKey) {
219
+ //TODO: ADD TIMEOUT WITH MESSAGE OF SUCCESS OR FAILURE.
220
+ //TODO IMPROVE CHECKS
221
+ if (this.writingKey !== this.bootstrap) {
222
+ console.log('Only bootstrap node can add indexer');
223
+ return;
224
+ }
225
+
226
+ const writerEntry = await this.base.view.get(peerTracPublicKey);
227
+ if (writerEntry === null || writerEntry.value.isValid === false || writerEntry.value.wk !== peerWritingKey || (writerEntry.value.isValid === true && writerEntry.value.isIndexer === true)) {
228
+ console.log(`Writer ${peerTracPublicKey}:${this.writingKey} cannot become an indexer`);
229
+ return;
230
+ }
231
+
232
+ const message = Buffer.concat([
233
+ Buffer.from(peerTracPublicKey, 'hex'),
234
+ Buffer.from(peerWritingKey, 'hex')
235
+ ]);
236
+
237
+ const indexerRequest = {
238
+ type: 'addIndexer',
239
+ key: this.wallet.publicKey,
240
+ value: {
241
+ ptpk: peerTracPublicKey,
242
+ pwk: peerWritingKey,
243
+ pop: this.wallet.sign(message)
244
+ }
245
+ }
246
+ await this.base.append(indexerRequest);
247
+ }
248
+ /**
249
+ * Removes the indexer status from a writer node.
250
+ * The node must already be a valid writer and an indexer to lose its indexer status.
251
+ * This operation can only be performed by the bootstrap node.
252
+ *
253
+ * @param {string} peerTracPublicKey - The TRAC public key of the writer node to lose indexer status.
254
+ * @param {string} peerWritingKey - The writing key of the writer node to lose indexer status.
255
+ * @returns {Promise<void>} Resolves once the writer has lost indexer status or if the operation fails.
256
+ */
257
+ async removeIndexer(peerTracPublicKey, peerWritingKey) {
258
+ //TODO: ADD TIMEOUT WITH MESSAGE OF SUCCESS OR FAILURE.
259
+ //TODO IMPROVE CHECKS
260
+ if (this.writingKey !== this.bootstrap) {
261
+ console.log('Only bootstrap node can remove indexer');
262
+ return;
263
+ }
264
+ const writerEntry = await this.base.view.get(peerTracPublicKey);
265
+
266
+ if (writerEntry === null || writerEntry.value.isValid === false || writerEntry.value.wk !== peerWritingKey || (writerEntry.value.isValid === true && writerEntry.value.isIndexer === false)) {
267
+ console.log(`Writer ${peerTracPublicKey}:${this.writingKey} cannot lose indexer status`);
268
+ return;
269
+ }
270
+
271
+ const message = Buffer.concat([
272
+ Buffer.from(peerTracPublicKey, 'hex'),
273
+ Buffer.from(peerWritingKey, 'hex')
274
+ ]);
275
+
276
+ const indexerRequest = {
277
+ type: 'removeIndexer',
278
+ key: this.wallet.publicKey,
279
+ value: {
280
+ ptpk: peerTracPublicKey,
281
+ pwk: peerWritingKey,
282
+ pop: this.wallet.sign(message)
283
+ }
284
+ }
285
+ await this.base.append(indexerRequest);
286
+ }
287
+
288
+ /**
289
+ * Retrieves the bootstrap entry from the base view.
290
+ *
291
+ * @returns {Promise<Object|null>} The bootstrap entry if it exists, otherwise null.
292
+ */
293
+ async getBootstrapEntry() {
294
+ return await this.base.view.get('bootstrap');
295
+ }
296
+
297
+ /**
298
+ * Handles incoming writer events by parsing the data and emitting a writerEvent.
299
+ *
300
+ * @param {MainSettlementBus} msbInstance - An instance of the MainSettlementBus class.
301
+ * @param {Buffer} data - The data received from the connection.
302
+ */
303
+ static async handleIncomingWriterEvent(msbInstance, data) {
304
+ try {
305
+ const bufferData = data.toString();
306
+ const parsedRequest = JSON.parse(bufferData);
307
+ if (parsedRequest.type === 'addWriter' || parsedRequest.type === 'removeWriter') {
308
+ msbInstance.emit('writerEvent', parsedRequest);
309
+ }
310
+ } catch (error) {
311
+ // for now ignore the error
312
+ }
313
+ }
314
+ }
315
+
316
+ export default WriterManager;