trac-msb 0.3.0-beta.1 → 0.3.0-beta.3
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": "msb.mjs",
|
|
4
|
-
"version": "0.3.0-beta.
|
|
4
|
+
"version": "0.3.0-beta.3",
|
|
5
5
|
"pear": {
|
|
6
6
|
"name": "trac-msb",
|
|
7
7
|
"type": "terminal"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"lint:fix": "eslint . --fix",
|
|
27
27
|
"protobuf": "node scripts/generate-protobufs.js",
|
|
28
28
|
"docs:check": "node scripts/docs/docs-checker.js",
|
|
29
|
-
"test:acceptance": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testTimeout=200000 tests/acceptance/v1/rpc.test.mjs tests/acceptance/v1/htlc/htlc-routing.test.mjs --runInBand",
|
|
29
|
+
"test:acceptance": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testTimeout=200000 tests/acceptance/v1/rpc.test.mjs tests/acceptance/v1/htlc/htlc-routing.test.mjs tests/acceptance/v1/current-epoch/current-epoch-routing.test.mjs tests/acceptance/v1/consensus-config/consensus-config-routing.test.mjs --runInBand",
|
|
30
30
|
"test:integration": "brittle-node -t 1200000 tests/integration/integration.test.js",
|
|
31
31
|
"test:unit:node": "brittle-node -t 60000 tests/unit/unit.test.js",
|
|
32
32
|
"test:unit:bare": "brittle-bare -t 60000 tests/unit/unit.test.js",
|
package/rpc/handlers.js
CHANGED
|
@@ -456,3 +456,21 @@ export async function handleAccountDetails({ msbInstance, respond, req }) {
|
|
|
456
456
|
stakedBalance: bufferToBigInt(nodeEntry.stakedBalance).toString(),
|
|
457
457
|
});
|
|
458
458
|
}
|
|
459
|
+
|
|
460
|
+
export async function handleCurrentEpoch({ req, msbInstance, respond }) {
|
|
461
|
+
const confirmed = getConfirmedParameter(buildRequestUrl(req));
|
|
462
|
+
if (confirmed === null) {
|
|
463
|
+
return respond(400, { error: 'Parameter "confirmed" must be exactly "true" or "false"' });
|
|
464
|
+
}
|
|
465
|
+
const current_epoch = await msbInstance.getCurrentEpoch(confirmed);
|
|
466
|
+
respond(200, { current_epoch });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export async function handleConsensusConfig({ req, msbInstance, respond }) {
|
|
470
|
+
const confirmed = getConfirmedParameter(buildRequestUrl(req));
|
|
471
|
+
if (confirmed === null) {
|
|
472
|
+
return respond(400, { error: 'Parameter "confirmed" must be exactly "true" or "false"' });
|
|
473
|
+
}
|
|
474
|
+
const consensusConfig = await msbInstance.getConsensusConfig(confirmed);
|
|
475
|
+
respond(200, consensusConfig);
|
|
476
|
+
}
|
package/rpc/routes/v1.js
CHANGED
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
handleHtlcClaim,
|
|
16
16
|
handleHtlcRefund,
|
|
17
17
|
handleHtlc,
|
|
18
|
+
handleCurrentEpoch,
|
|
19
|
+
handleConsensusConfig,
|
|
18
20
|
} from '../handlers.js';
|
|
19
21
|
|
|
20
22
|
export const v1Routes = [
|
|
@@ -34,4 +36,6 @@ export const v1Routes = [
|
|
|
34
36
|
{ method: 'POST', path: '/tx-payloads-bulk', handler: handleFetchBulkTxPayloads },
|
|
35
37
|
{ method: 'GET', path: '/tx/details', handler: handleTransactionExtendedDetails },
|
|
36
38
|
{ method: 'GET', path: '/account', handler: handleAccountDetails },
|
|
39
|
+
{ method: 'GET', path: '/current-epoch', exact: true, handler: handleCurrentEpoch },
|
|
40
|
+
{ method: 'GET', path: '/consensus-config', exact: true, handler: handleConsensusConfig },
|
|
37
41
|
];
|
|
@@ -42,9 +42,9 @@ class ValidatorMessageOrchestrator {
|
|
|
42
42
|
*
|
|
43
43
|
* @param {object} [message] Outgoing operation payload.
|
|
44
44
|
* @param {string} [message.address] Requester address (bech32m).
|
|
45
|
-
* @returns {string|null} Selected validator public key hex, or null when unavailable.
|
|
45
|
+
* @returns {Promise<string|null>} Selected validator public key hex, or null when unavailable.
|
|
46
46
|
*/
|
|
47
|
-
#pickValidatorForMessage(message) {
|
|
47
|
+
async #pickValidatorForMessage(message) {
|
|
48
48
|
const requesterAddress = message?.address;
|
|
49
49
|
|
|
50
50
|
if (!requesterAddress) return null
|
|
@@ -58,10 +58,15 @@ class ValidatorMessageOrchestrator {
|
|
|
58
58
|
return publicKeyToAddress(publicKey, this.#config) !== requesterAddress;
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
const pool = eligible.length > 0 ? eligible : connected;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
const pool = [...(eligible.length > 0 ? eligible : connected)];
|
|
62
|
+
while (pool.length > 0) {
|
|
63
|
+
const index = Math.floor(Math.random() * pool.length);
|
|
64
|
+
const [publicKey] = pool.splice(index, 1);
|
|
65
|
+
const address = publicKeyToAddress(publicKey, this.#config);
|
|
66
|
+
if (await this.state.isValidatorAddress(address)) return publicKey;
|
|
67
|
+
this.validatorConnectionManager.remove(publicKey);
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
/**
|
|
@@ -76,7 +81,7 @@ class ValidatorMessageOrchestrator {
|
|
|
76
81
|
return false;
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
const validatorPublicKey = this.#pickValidatorForMessage(message);
|
|
84
|
+
const validatorPublicKey = await this.#pickValidatorForMessage(message);
|
|
80
85
|
if (!validatorPublicKey) return false;
|
|
81
86
|
console.log("Sending message to validator:", publicKeyToAddress(validatorPublicKey, this.#config));
|
|
82
87
|
|
|
@@ -91,6 +96,7 @@ class ValidatorMessageOrchestrator {
|
|
|
91
96
|
|
|
92
97
|
// TODO: After Legacy is deprecated, we don't need to check preferred protocol here.
|
|
93
98
|
const validatorConnection = this.validatorConnectionManager.getConnection(validatorPublicKey);
|
|
99
|
+
if (!validatorConnection) return this.send(message, retries + 1);
|
|
94
100
|
const preferredProtocol = validatorConnection.protocolSessions.validator.preferredProtocol;
|
|
95
101
|
let success = false;
|
|
96
102
|
if (preferredProtocol === validatorConnection.protocolSessions.validator.supportedProtocols.LEGACY) {
|
|
@@ -4,6 +4,7 @@ import b4a from "b4a";
|
|
|
4
4
|
import { bufferToAddress } from "../../../core/state/utils/address.js";
|
|
5
5
|
import tracCryptoApi from "trac-crypto-api";
|
|
6
6
|
import { WRITER_BYTE_LENGTH, CONNECTION_STATUS } from "../../../utils/constants.js";
|
|
7
|
+
import { publicKeyToAddress } from "../../../utils/helpers.js";
|
|
7
8
|
|
|
8
9
|
// Internal constants
|
|
9
10
|
const VALIDATOR_CANDIDATES_PER_CYCLE = 10;
|
|
@@ -48,27 +49,19 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
48
49
|
* - Assumes writers list is already pre-filtered and deduplicated from scan phase.
|
|
49
50
|
* - Excludes self.
|
|
50
51
|
* - Excludes already connected or pending nodes.
|
|
51
|
-
* -
|
|
52
|
+
* - Revalidates roles and the admin threshold before connecting.
|
|
52
53
|
* - Randomizes selection (Fisher-Yates) to avoid bias.
|
|
53
54
|
* @param {Array<Object>} writers - The deduplicated list of active writers.
|
|
54
|
-
* @param {Object} adminEntry - The current admin node entry.
|
|
55
55
|
* @returns {{candidates: Array<Object>, slots: number}} The selected candidates and the number of available connection slots.
|
|
56
56
|
*/
|
|
57
|
-
#selectCandidates(writers
|
|
57
|
+
#selectCandidates(writers) {
|
|
58
58
|
const manager = this.#network.validatorConnectionManager;
|
|
59
|
-
const adminThreshold = this.#config.maxWritersForAdminIndexerConnection;
|
|
60
|
-
|
|
61
|
-
// Evaluates the admin blockage policy based on fully connected/alive writers
|
|
62
|
-
const aliveWritersCount = this.#countAliveWriters(writers, adminEntry);
|
|
63
|
-
const adminBlocked = adminEntry && aliveWritersCount >= adminThreshold;
|
|
64
|
-
|
|
65
59
|
// Target calculation: ensures the observer deducts its own slot from the network size
|
|
66
60
|
// only if it belongs to the valid writers list.
|
|
67
61
|
const isSelfInWriters = writers.some(w => w.address === this.#address);
|
|
68
62
|
const networkSize = isSelfInWriters ? writers.length - 1 : writers.length;
|
|
69
63
|
const target = Math.min(networkSize, this.#config.maxValidators);
|
|
70
64
|
|
|
71
|
-
const isSelfAdmin = this.#address === adminEntry?.address;
|
|
72
65
|
const totalConnected = manager.connectionCount();
|
|
73
66
|
|
|
74
67
|
const available = writers.filter((w) => {
|
|
@@ -82,9 +75,6 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
82
75
|
// Skips nodes we are already interacting with
|
|
83
76
|
if (isConnected || isPending) return false;
|
|
84
77
|
|
|
85
|
-
// The admin is actively filtered out from the pool if the threshold policy is triggered
|
|
86
|
-
if (adminBlocked && !isSelfAdmin && w.address === adminEntry?.address) return false;
|
|
87
|
-
|
|
88
78
|
return true;
|
|
89
79
|
});
|
|
90
80
|
|
|
@@ -107,37 +97,6 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
107
97
|
}
|
|
108
98
|
}
|
|
109
99
|
|
|
110
|
-
/**
|
|
111
|
-
* Enforces admin-specific connection rules.
|
|
112
|
-
*
|
|
113
|
-
* If the count of fully connected writers exceeds the threshold:
|
|
114
|
-
* - The admin node is excluded from the validator pool (handled in selection phase).
|
|
115
|
-
* - Non-admin nodes detach the admin from the validator pool, preserving replication.
|
|
116
|
-
*
|
|
117
|
-
* NOTE:
|
|
118
|
-
* The admin is not forcibly disconnected from all roles.
|
|
119
|
-
* It can still maintain connections for tx submission and observation.
|
|
120
|
-
* @param {Array<Object>} writers - The deduplicated list of active writers.
|
|
121
|
-
* @param {Object} adminEntry - The current admin node entry.
|
|
122
|
-
*/
|
|
123
|
-
#enforceAdminPolicy(writers, adminEntry) {
|
|
124
|
-
// Evaluates against alive network peers rather than dormant database entries
|
|
125
|
-
const aliveWritersCount = this.#countAliveWriters(writers, adminEntry);
|
|
126
|
-
|
|
127
|
-
if (aliveWritersCount < this.#config.maxWritersForAdminIndexerConnection) return;
|
|
128
|
-
|
|
129
|
-
this.#logger.debug("Validator threshold reached → enforcing admin policy");
|
|
130
|
-
|
|
131
|
-
const isAdmin = adminEntry && this.#address === adminEntry.address;
|
|
132
|
-
if (isAdmin) return;
|
|
133
|
-
|
|
134
|
-
// Non-admin nodes: ensure the admin is not kept as a validator connection
|
|
135
|
-
const adminWriter = writers.find((w) => w.address === adminEntry?.address);
|
|
136
|
-
if (adminWriter?.publicKey) {
|
|
137
|
-
this.#network.validatorConnectionManager.remove(adminWriter.publicKey);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
100
|
/**
|
|
142
101
|
* Removes active connections to writers that are no longer valid (e.g., removed/soft-deleted).
|
|
143
102
|
* @param {Uint8Array} publicKey - The public key of the stale connection to be removed.
|
|
@@ -161,40 +120,6 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
161
120
|
return !this.isInterrupted;
|
|
162
121
|
}
|
|
163
122
|
|
|
164
|
-
/**
|
|
165
|
-
* Counts the number of physically active and connected writers in the network.
|
|
166
|
-
* This method bridges the gap between the Autobase state (which may include
|
|
167
|
-
* offline/dormant writers due to its append-only/soft-delete nature) and the
|
|
168
|
-
* actual physical network layer. It ensures the admin threshold policy is
|
|
169
|
-
* evaluated against truly active nodes rather than historical state.
|
|
170
|
-
*
|
|
171
|
-
* @param {Array<Object>} writers - The deduplicated list of writers from the Autobase.
|
|
172
|
-
* @param {Object} adminEntry - The current admin node entry.
|
|
173
|
-
* @returns {number} The count of alive, fully connected writers (including self).
|
|
174
|
-
*/
|
|
175
|
-
#countAliveWriters(writers, adminEntry) {
|
|
176
|
-
let aliveCount = 0;
|
|
177
|
-
const manager = this.#network.validatorConnectionManager;
|
|
178
|
-
|
|
179
|
-
for (const w of writers) {
|
|
180
|
-
// Ignores the admin in the threshold calculation
|
|
181
|
-
if (w.address === adminEntry?.address) continue;
|
|
182
|
-
|
|
183
|
-
// Always counts the current node as alive if it's in the valid writers list
|
|
184
|
-
if (w.address === this.#address) {
|
|
185
|
-
aliveCount++;
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Counts only fully connected nodes (ignores pending or offline peers)
|
|
190
|
-
if (manager.connected(w.publicKey)) {
|
|
191
|
-
aliveCount++;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return aliveCount;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
123
|
/**
|
|
199
124
|
* Starts the validator observer loop.
|
|
200
125
|
* Initializes the scheduler and bootstrap state.
|
|
@@ -226,8 +151,8 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
226
151
|
* Main worker loop (executed periodically).
|
|
227
152
|
*
|
|
228
153
|
* Flow:
|
|
229
|
-
* 1.
|
|
230
|
-
* 2.
|
|
154
|
+
* 1. Revalidate connected peers against current roles and the admin threshold.
|
|
155
|
+
* 2. Fetch admin + writers (cached).
|
|
231
156
|
* 3. Skip if the connection pool is full.
|
|
232
157
|
* 4. Select candidates.
|
|
233
158
|
* 5. Attempt connections (with pacing).
|
|
@@ -245,18 +170,23 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
245
170
|
}
|
|
246
171
|
|
|
247
172
|
try {
|
|
173
|
+
// Recheck bounded connection pools on every cycle, even when discovery
|
|
174
|
+
// is cached or the pool is full. Removing a role preserves replication.
|
|
175
|
+
const manager = this.#network.validatorConnectionManager;
|
|
176
|
+
for (const publicKey of manager.connectedPeers()) {
|
|
177
|
+
if (!await this.#state.isValidatorAddress(publicKeyToAddress(publicKey, this.#config))) {
|
|
178
|
+
manager.remove(publicKey);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
248
181
|
const adminEntry = await this.#getAdminEntryCached();
|
|
249
182
|
const writers = await this.#getWritersCached(adminEntry);
|
|
250
183
|
|
|
251
184
|
if (writers.length === 0) return next(interval);
|
|
252
185
|
|
|
253
|
-
this.#enforceAdminPolicy(writers, adminEntry);
|
|
254
|
-
|
|
255
|
-
const manager = this.#network.validatorConnectionManager;
|
|
256
186
|
// CEILING: do not exceed the max connections limit
|
|
257
187
|
if (manager.maxConnectionsReached()) return next(interval);
|
|
258
188
|
|
|
259
|
-
const { candidates, slots} = this.#selectCandidates(writers
|
|
189
|
+
const { candidates, slots} = this.#selectCandidates(writers);
|
|
260
190
|
if (candidates.length > 0) {
|
|
261
191
|
await this.#processCandidates(candidates, slots);
|
|
262
192
|
}
|
|
@@ -389,7 +319,8 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
389
319
|
const addressBuffer = await this.#state.getRegisteredWriterKey(writerKeyHex);
|
|
390
320
|
|
|
391
321
|
if (!addressBuffer || b4a.byteLength(addressBuffer) !== this.#config.addressLength) {
|
|
392
|
-
|
|
322
|
+
// Autobase membership can arrive before the signed address mapping.
|
|
323
|
+
// Retry on the next scan instead of caching a transient absence forever.
|
|
393
324
|
continue;
|
|
394
325
|
}
|
|
395
326
|
|
|
@@ -410,9 +341,7 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
410
341
|
|
|
411
342
|
const { addr, publicKey, publicKeyHex } = decodedIdentity;
|
|
412
343
|
|
|
413
|
-
if (value.isIndexer && addr !== adminEntry?.address)
|
|
414
|
-
|
|
415
|
-
if (value.isRemoved) {
|
|
344
|
+
if (value.isRemoved || (value.isIndexer && addr !== adminEntry?.address)) {
|
|
416
345
|
removedMap.set(addr, publicKey);
|
|
417
346
|
continue;
|
|
418
347
|
}
|
|
@@ -436,7 +365,7 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
436
365
|
/**
|
|
437
366
|
* Attempts to establish a connection with a validator candidate.
|
|
438
367
|
*
|
|
439
|
-
*
|
|
368
|
+
* Revalidates the role because discovery may predate a promotion or removal.
|
|
440
369
|
*
|
|
441
370
|
* Behavior:
|
|
442
371
|
* - Delegates connection attempt to the network layer.
|
|
@@ -445,6 +374,7 @@ class ValidatorObserverService extends SchedulableService {
|
|
|
445
374
|
*/
|
|
446
375
|
async #tryConnect(candidate) {
|
|
447
376
|
try {
|
|
377
|
+
if (!await this.#state.isValidatorAddress(candidate.address)) return CONNECTION_STATUS.IGNORED;
|
|
448
378
|
return await this.#network.tryConnect(candidate.publicKeyHex, "validator");
|
|
449
379
|
} catch (err) {
|
|
450
380
|
this.#logger.error(`Validator connection attempt failed: ${err.message}`);
|
package/src/core/state/State.js
CHANGED
|
@@ -418,13 +418,26 @@ class State extends ReadyResource {
|
|
|
418
418
|
}
|
|
419
419
|
|
|
420
420
|
async isAdminAllowedToValidate() {
|
|
421
|
-
if (!this.
|
|
422
|
-
|
|
423
|
-
const isAdmin = this.writingKey.toString('hex') === this.#config.bootstrap.toString('hex');
|
|
424
|
-
const isIndexer = this.isIndexer();
|
|
421
|
+
if (!this.isWritable() || !this.isIndexer() || !await this.isAdmin()) return false;
|
|
425
422
|
const activeWriters = await this.getActiveWriterCount(true);
|
|
426
|
-
|
|
427
|
-
|
|
423
|
+
return activeWriters < this.#config.maxWritersForAdminIndexerConnection;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Checks a remote validator against the current local view, including pending
|
|
428
|
+
* role changes. Cached discovery and established transports do not grant a role.
|
|
429
|
+
*/
|
|
430
|
+
async isValidatorAddress(address) {
|
|
431
|
+
const entry = await this.getNodeEntryUnsigned(address);
|
|
432
|
+
if (!entry?.isWriter) return false;
|
|
433
|
+
if (!entry.isIndexer) {
|
|
434
|
+
const signedEntry = await this.getNodeEntry(address);
|
|
435
|
+
return !!signedEntry?.isWriter && !signedEntry.isIndexer;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const admin = await this.getAdminEntry();
|
|
439
|
+
if (address !== admin?.address || !entry.wk || !admin.wk || !b4a.equals(entry.wk, admin.wk)) return false;
|
|
440
|
+
return await this.getActiveWriterCount(true) < this.#config.maxWritersForAdminIndexerConnection;
|
|
428
441
|
}
|
|
429
442
|
|
|
430
443
|
async isAddressWhitelisted(address) {
|
|
@@ -676,6 +689,12 @@ class State extends ReadyResource {
|
|
|
676
689
|
for await (const entry of historyStream) {
|
|
677
690
|
if (isTransactionRecordPut(entry)) {
|
|
678
691
|
hashes.push({ hash: entry.key, confirmed_length: entry.seq });
|
|
692
|
+
} else if (entry.type === 'put' && entry.key.startsWith(EntryType.EPOCH_HASH)) {
|
|
693
|
+
// Epoch records share this history, but have their own key namespace.
|
|
694
|
+
const hash = entry.key.slice(EntryType.EPOCH_HASH.length);
|
|
695
|
+
if (hash.length === HASH_BYTE_LENGTH * 2 && isHexString(hash)) {
|
|
696
|
+
hashes.push({ hash, confirmed_length: entry.seq });
|
|
697
|
+
}
|
|
679
698
|
}
|
|
680
699
|
}
|
|
681
700
|
|
|
@@ -5561,6 +5580,28 @@ class State extends ReadyResource {
|
|
|
5561
5580
|
return Status.SUCCESS;
|
|
5562
5581
|
}
|
|
5563
5582
|
|
|
5583
|
+
async getUnsignedConsensusConfig() {
|
|
5584
|
+
// Keep the pointer and record in the same view while unsigned state can change.
|
|
5585
|
+
const snapshot = this.#base.view.snapshot();
|
|
5586
|
+
try {
|
|
5587
|
+
const currentConfigPointer = (await snapshot.get(EntryType.CONSENSUS_CONFIG_CURRENT))?.value;
|
|
5588
|
+
if (_.isNil(currentConfigPointer)) return null;
|
|
5589
|
+
|
|
5590
|
+
if (!b4a.isBuffer(currentConfigPointer) || currentConfigPointer.length !== CONSENSUS_CONFIG_INDEX_SIZE) {
|
|
5591
|
+
throw new Error('Invalid current consensus config pointer.');
|
|
5592
|
+
}
|
|
5593
|
+
|
|
5594
|
+
const currentConfigIndex = currentConfigPointer.readUInt32BE(0);
|
|
5595
|
+
const encodedConsensusConfig = (await snapshot.get(EntryType.CONSENSUS_CONFIG_RECORD + currentConfigIndex))?.value;
|
|
5596
|
+
if (_.isNil(encodedConsensusConfig)) {
|
|
5597
|
+
throw new Error(`Consensus config record ${currentConfigIndex} does not exist.`);
|
|
5598
|
+
}
|
|
5599
|
+
|
|
5600
|
+
return decodeVersionedConsensusConfig(encodedConsensusConfig);
|
|
5601
|
+
} finally {
|
|
5602
|
+
await snapshot.close();
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5564
5605
|
}
|
|
5565
5606
|
|
|
5566
5607
|
export default State;
|
package/src/index.js
CHANGED
|
@@ -37,7 +37,8 @@ import {
|
|
|
37
37
|
encodeApplyOperation,
|
|
38
38
|
encodeConsensusConfig,
|
|
39
39
|
safeDecodeApplyOperation,
|
|
40
|
-
safeEncodeApplyOperation
|
|
40
|
+
safeEncodeApplyOperation,
|
|
41
|
+
decodeEpochRecord
|
|
41
42
|
} from "./codecs/apply/applyOperationCodec.js";
|
|
42
43
|
import { encodeVdfConfig } from "./codecs/consensus/v1/vdfConfigCodec.js";
|
|
43
44
|
import PartialTransactionValidator from "./core/network/protocols/validators/shared/validators/PartialTransactionValidator.js";
|
|
@@ -425,7 +426,16 @@ export class MainSettlementBus extends ReadyResource {
|
|
|
425
426
|
|
|
426
427
|
async fetchBulkTxPayloads(hashes) {
|
|
427
428
|
const response = { results: [], missing: [] };
|
|
428
|
-
const results = await Promise.all(hashes.map((hash) =>
|
|
429
|
+
const results = await Promise.all(hashes.map(async (hash) => {
|
|
430
|
+
const transaction = await this.getConfirmedTxInfo(hash);
|
|
431
|
+
if (transaction) return transaction.decoded;
|
|
432
|
+
|
|
433
|
+
const epochRecord = await this.#state.getEpochProof(hash);
|
|
434
|
+
if (epochRecord === null) return null;
|
|
435
|
+
|
|
436
|
+
// This hash identifies the stored epoch record, not an original transaction.
|
|
437
|
+
return { type: OperationType.SET_EPOCH, seo: decodeEpochRecord(epochRecord) };
|
|
438
|
+
}));
|
|
429
439
|
|
|
430
440
|
results.forEach((result, index) => {
|
|
431
441
|
const hash = hashes[index];
|
|
@@ -434,7 +444,7 @@ export class MainSettlementBus extends ReadyResource {
|
|
|
434
444
|
return;
|
|
435
445
|
}
|
|
436
446
|
|
|
437
|
-
const decodedResult = normalizeDecodedPayloadForJson(result
|
|
447
|
+
const decodedResult = normalizeDecodedPayloadForJson(result, this.#config);
|
|
438
448
|
response.results.push({ hash, payload: decodedResult });
|
|
439
449
|
});
|
|
440
450
|
|
|
@@ -1290,6 +1300,19 @@ export class MainSettlementBus extends ReadyResource {
|
|
|
1290
1300
|
expectedKeys.every(key => Object.hasOwn(value, key))
|
|
1291
1301
|
);
|
|
1292
1302
|
}
|
|
1303
|
+
|
|
1304
|
+
async getCurrentEpoch(confirmed = true) {
|
|
1305
|
+
const epoch = confirmed
|
|
1306
|
+
? await this.#state.getCurrentEpoch()
|
|
1307
|
+
: await this.#state.getCurrentEpochUnsigned();
|
|
1308
|
+
return epoch === null ? null : epoch.toString();
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
async getConsensusConfig(confirmed = true) {
|
|
1312
|
+
return confirmed
|
|
1313
|
+
? this.#state.getSignedConsensusConfig()
|
|
1314
|
+
: this.#state.getUnsignedConsensusConfig();
|
|
1315
|
+
}
|
|
1293
1316
|
}
|
|
1294
1317
|
|
|
1295
1318
|
export default MainSettlementBus;
|