trac-msb 0.2.24 → 0.3.0-beta.1

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.
Files changed (137) hide show
  1. package/SECURITY.md +2 -2
  2. package/cli/commandHandler.js +317 -20
  3. package/cli/handlers.js +9 -1
  4. package/cli/interactive.js +4 -3
  5. package/cli/tokenSupplyAudit.js +90 -0
  6. package/msb.mjs +1 -0
  7. package/package.json +15 -6
  8. package/rpc/create_server.js +12 -3
  9. package/rpc/handlers.js +103 -0
  10. package/rpc/routes/v1.js +8 -0
  11. package/src/codecs/apply/applyOperationCodec.js +334 -0
  12. package/src/codecs/apply/applyOperations.generated.cjs +6380 -0
  13. package/src/codecs/consensus/v1/consensusV1.generated.cjs +3508 -0
  14. package/src/codecs/consensus/v1/consensusV1OperationCodec.js +161 -0
  15. package/src/codecs/consensus/v1/vdfConfigCodec.js +83 -0
  16. package/src/{utils/protobuf → codecs/network/v1}/networkV1.generated.cjs +664 -393
  17. package/src/codecs/network/v1/networkV1OperationCodec.js +25 -0
  18. package/src/codecs/protobufRuntime.cjs +95 -0
  19. package/src/codecs/state/escrowEntryCodec.js +45 -0
  20. package/src/codecs/state/state.generated.cjs +659 -0
  21. package/src/config/args.js +0 -1
  22. package/src/config/config.js +46 -7
  23. package/src/config/env.js +33 -6
  24. package/src/core/consensus/epochAppenderSelection/EpochAppenderSelectionError.js +55 -0
  25. package/src/core/consensus/epochAppenderSelection/EpochAppenderSelectionOperations.js +118 -0
  26. package/src/core/consensus/epochAppenderSelection/EpochAppenderSelector.js +1042 -0
  27. package/src/core/consensus/epochAppenderSelection/createEpochAppenderSelector.js +23 -0
  28. package/src/core/consensus/epochAppenderSelection/epochAppenderSelectionLedgerReader.js +274 -0
  29. package/src/core/consensus/epochAppenderSelection/epochAppenderSelectionState.js +488 -0
  30. package/src/core/consensus/epochAppenderSelection/epochAppenderSelectionStorage.js +93 -0
  31. package/src/core/consensus/quorum.js +11 -0
  32. package/src/core/consensus/services/EpochCoordinationRound.js +839 -0
  33. package/src/core/consensus/services/EpochCoordinatorOperations.js +211 -0
  34. package/src/core/consensus/services/EpochCoordinatorService.js +335 -0
  35. package/src/core/consensus/services/EpochRoundListeners.js +116 -0
  36. package/src/core/consensus/services/EpochStateMachine.js +141 -0
  37. package/src/core/consensus/services/VDFBare.js +291 -0
  38. package/src/core/consensus/services/VDFNode.js +111 -0
  39. package/src/core/consensus/services/VDFProgressWatchdog.js +79 -0
  40. package/src/core/consensus/services/VDFService.js +87 -0
  41. package/src/core/consensus/services/VDFServiceManager.js +147 -0
  42. package/src/core/consensus/services/createVDFService.js +16 -0
  43. package/src/core/consensus/services/vdf-guardian.js +32 -0
  44. package/src/core/consensus/services/vdf-worker.js +90 -0
  45. package/src/core/network/Network.js +327 -238
  46. package/src/core/network/protocols/consensus/IndexerMessages.js +72 -0
  47. package/src/core/network/protocols/consensus/v1/ConsensusV1ConnectionPolicy.js +57 -0
  48. package/src/core/network/protocols/consensus/v1/ConsensusV1MessageRouter.js +147 -0
  49. package/src/core/network/protocols/consensus/v1/ConsensusV1Protocol.js +72 -0
  50. package/src/core/network/protocols/consensus/v1/IndexerPendingRequestService.js +108 -0
  51. package/src/core/network/protocols/consensus/v1/V1ConsensusProtocolError.js +29 -0
  52. package/src/core/network/protocols/consensus/v1/handlers/V1EpochAppendOperationHandler.js +68 -0
  53. package/src/core/network/protocols/consensus/v1/handlers/V1EpochProofProposalOperationHandler.js +226 -0
  54. package/src/core/network/protocols/consensus/v1/validators/ConsensusValidationSchema.js +192 -0
  55. package/src/core/network/protocols/consensus/v1/validators/V1BaseConsensusOperation.js +428 -0
  56. package/src/core/network/protocols/consensus/v1/validators/V1EpochAppendReply.js +47 -0
  57. package/src/core/network/protocols/consensus/v1/validators/V1EpochAppendRequest.js +17 -0
  58. package/src/core/network/protocols/consensus/v1/validators/V1EpochProofProposalApproval.js +116 -0
  59. package/src/core/network/protocols/consensus/v1/validators/V1EpochProofProposalRequest.js +152 -0
  60. package/src/core/network/protocols/consensus/v1/validators/consensusValidationSchemaHelpers.js +145 -0
  61. package/src/core/network/protocols/shared/ConnectionOperationHandler.js +27 -0
  62. package/src/core/network/protocols/{NetworkMessages.js → validators/ValidatorMessages.js} +24 -14
  63. package/src/core/network/protocols/{ProtocolSession.js → validators/ValidatorProtocolSession.js} +22 -25
  64. package/src/core/network/protocols/{legacy/NetworkMessageRouter.js → validators/legacy/ValidatorLegacyMessageRouter.js} +6 -6
  65. package/src/core/network/protocols/{LegacyProtocol.js → validators/legacy/ValidatorLegacyProtocol.js} +9 -7
  66. package/src/core/network/protocols/{legacy → validators/legacy}/handlers/BaseStateOperationHandler.js +1 -1
  67. package/src/core/network/protocols/{legacy → validators/legacy}/handlers/LegacyGetRequestHandler.js +5 -5
  68. package/src/core/network/protocols/{legacy → validators/legacy}/handlers/LegacyRoleOperationHandler.js +5 -5
  69. package/src/core/network/protocols/{legacy → validators/legacy}/handlers/LegacySubnetworkOperationHandler.js +5 -5
  70. package/src/core/network/protocols/{legacy → validators/legacy}/handlers/LegacyTransferOperationHandler.js +5 -5
  71. package/src/core/network/protocols/{ProtocolInterface.js → validators/shared/ValidatorProtocolInterface.js} +5 -5
  72. package/src/core/network/protocols/{shared → validators/shared}/validators/PartialBootstrapDeploymentValidator.js +1 -1
  73. package/src/core/network/protocols/validators/shared/validators/PartialHtlcValidator.js +325 -0
  74. package/src/core/network/protocols/{shared → validators/shared}/validators/PartialOperationValidator.js +44 -21
  75. package/src/core/network/protocols/{shared → validators/shared}/validators/PartialRoleAccessValidator.js +3 -3
  76. package/src/core/network/protocols/{shared → validators/shared}/validators/PartialTransactionValidator.js +3 -3
  77. package/src/core/network/protocols/{shared → validators/shared}/validators/PartialTransferValidator.js +4 -4
  78. package/src/core/network/protocols/{v1 → validators/v1}/V1ProtocolError.js +1 -1
  79. package/src/core/network/{services/PendingRequestService.js → protocols/validators/v1/ValidatorPendingRequestService.js} +19 -82
  80. package/src/core/network/protocols/{v1/NetworkMessageRouter.js → validators/v1/ValidatorV1MessageRouter.js} +9 -9
  81. package/src/core/network/protocols/{V1Protocol.js → validators/v1/ValidatorV1Protocol.js} +10 -8
  82. package/src/core/network/protocols/{connectionPolicies.js → validators/v1/connectionPolicies.js} +1 -1
  83. package/src/core/network/protocols/{v1 → validators/v1}/handlers/V1BaseOperationHandler.js +6 -23
  84. package/src/core/network/protocols/{v1 → validators/v1}/handlers/V1BroadcastTransactionOperationHandler.js +14 -9
  85. package/src/core/network/protocols/{v1 → validators/v1}/handlers/V1LivenessOperationHandler.js +5 -4
  86. package/src/core/network/protocols/{v1 → validators/v1}/handlers/broadcastTransaction/BroadcastTransactionErrorMapper.js +3 -3
  87. package/src/core/network/protocols/{v1 → validators/v1}/handlers/broadcastTransaction/BroadcastTransactionOperationStrategies.js +44 -1
  88. package/src/core/network/protocols/{v1 → validators/v1}/handlers/broadcastTransaction/BroadcastTransactionPayloadUtils.js +1 -1
  89. package/src/core/network/protocols/{v1 → validators/v1}/validators/V1BaseOperation.js +19 -15
  90. package/src/core/network/protocols/{v1 → validators/v1}/validators/V1BroadcastTransactionRequest.js +2 -2
  91. package/src/core/network/protocols/{v1 → validators/v1}/validators/V1BroadcastTransactionResponse.js +34 -18
  92. package/src/core/network/protocols/{v1 → validators/v1}/validators/V1ValidationSchema.js +3 -3
  93. package/src/core/network/services/IndexerConnectionManager.js +88 -0
  94. package/src/core/network/services/TransactionPoolService.js +11 -18
  95. package/src/core/network/services/TransactionRateLimiterService.js +1 -1
  96. package/src/core/network/services/ValidatorConnectionManager.js +163 -0
  97. package/src/core/network/services/{MessageOrchestrator.js → ValidatorMessageOrchestrator.js} +34 -42
  98. package/src/core/network/services/ValidatorObserverService.js +21 -36
  99. package/src/core/network/shared/BasePendingRequestService.js +67 -0
  100. package/src/core/network/shared/PeerConnectionManager.js +185 -0
  101. package/src/core/state/ConfirmationWatch.js +82 -0
  102. package/src/core/state/State.js +1794 -173
  103. package/src/core/state/StateEventQueue.js +69 -0
  104. package/src/core/state/utils/address.js +10 -6
  105. package/src/core/state/utils/epochProof.js +109 -0
  106. package/src/core/state/utils/escrowEntry.js +178 -0
  107. package/src/core/state/validators/StateValidationSchema.js +757 -0
  108. package/src/core/state/validators/stateValidationRules.js +312 -0
  109. package/src/index.js +255 -65
  110. package/src/messages/consensus/v1/ConsensusMessageBuilder.js +368 -0
  111. package/src/messages/consensus/v1/ConsensusMessageDirector.js +142 -0
  112. package/src/messages/consensus/v1/consensusMessageFactory.js +12 -0
  113. package/src/messages/consensus/v1/epochAppendMessage.js +58 -0
  114. package/src/messages/network/v1/NetworkMessageBuilder.js +15 -7
  115. package/src/messages/state/ApplyStateMessageBuilder.js +499 -13
  116. package/src/messages/state/ApplyStateMessageDirector.js +261 -2
  117. package/src/utils/StateMachine.js +125 -0
  118. package/src/utils/applyOperations.js +45 -2
  119. package/src/utils/buffer.js +239 -59
  120. package/src/utils/consensusConfig.js +76 -0
  121. package/src/utils/constants.js +174 -73
  122. package/src/utils/helpers.js +0 -25
  123. package/src/utils/htlcLock.js +122 -0
  124. package/src/utils/logger.js +10 -4
  125. package/src/utils/migrationUtils.js +2 -1
  126. package/src/utils/normalizers.js +113 -0
  127. package/src/utils/scheduler/SchedulableService.js +107 -0
  128. package/src/utils/{Scheduler.js → scheduler/Scheduler.js} +56 -17
  129. package/src/core/network/services/ConnectionManager.js +0 -406
  130. package/src/utils/check.js +0 -525
  131. package/src/utils/protobuf/applyOperations.cjs +0 -1373
  132. package/src/utils/protobuf/operationHelpers.js +0 -81
  133. /package/src/core/network/protocols/{legacy → validators/legacy}/handlers/LegacyResponseHandler.js +0 -0
  134. /package/src/core/network/protocols/{legacy → validators/legacy}/validators/ValidatorResponse.js +0 -0
  135. /package/src/core/network/protocols/{legacy → validators/legacy}/validators/base/BaseResponse.js +0 -0
  136. /package/src/core/network/protocols/{v1 → validators/v1}/validators/V1LivenessRequest.js +0 -0
  137. /package/src/core/network/protocols/{v1 → validators/v1}/validators/V1LivenessResponse.js +0 -0
@@ -0,0 +1,211 @@
1
+ /**
2
+ * @typedef {object} EpochVdfProof
3
+ * @property {Buffer} solution Encoded Wesolowski proof.
4
+ * @property {number} difficulty VDF iteration count from the signed consensus config.
5
+ * @property {number} discriminantSizeBits Discriminant size used for this calculation.
6
+ *
7
+ * @typedef {object} EpochProofApproval
8
+ * @property {Buffer} signature Verified proof signature from another indexer.
9
+ * @property {Buffer} approver Signer address stored as a buffer.
10
+ *
11
+ * @typedef {object} EpochApprovalCollection
12
+ * @property {Array<{key: Buffer}>} approvers Other indexers that can approve this proof.
13
+ * @property {EpochProofApproval[]} approvals Accepted proof signatures from this attempt.
14
+ * @property {Array<{member: {key: Buffer}, error: Error}>} rejections Failed requests from this attempt.
15
+ * @property {boolean} closed True when enough signatures are collected or this attempt ends.
16
+ */
17
+ import tracCryptoApi from "trac-crypto-api";
18
+ import { generateUUID } from '../../../utils/helpers.js';
19
+ import { consensusMessageFactory } from '../../../messages/consensus/v1/consensusMessageFactory.js';
20
+ import addressUtils from '../../state/utils/address.js';
21
+ import b4a from "b4a";
22
+ import { encodeProofProposal, encodeProofProposalApproval } from '../../../codecs/consensus/v1/consensusV1OperationCodec.js';
23
+ import { applyStateMessageFactory } from '../../../messages/state/applyStateMessageFactory.js';
24
+ import { toHex, uint16ToBuffer, uint32ToBuffer } from '../../../utils/buffer.js';
25
+ import { encodeApplyOperation } from '../../../codecs/apply/applyOperationCodec.js';
26
+ import { ConsensusResultCode } from '../../../utils/constants.js';
27
+
28
+ /**
29
+ * Builds proof messages and SET_EPOCH payloads, requests signatures and starts appends. The round uses
30
+ * these methods and controls the FSM. The selector decides whether this node may append.
31
+ */
32
+ export class EpochCoordinatorOperations {
33
+ #state;
34
+ #wallet;
35
+ #vdfService;
36
+ #config;
37
+
38
+ /**
39
+ * Sets up ledger access, the signing wallet, the VDF worker and message config.
40
+ *
41
+ * @param {import('../../state/State.js').default} state Signed ledger reads and the write queue that checks append authorization.
42
+ * @param {import('./VDFService.js').VDFService} vdfService VDF service ready to calculate proofs.
43
+ * @param {object} wallet Local signing wallet.
44
+ * @param {object} config Network ID and address encoding config.
45
+ */
46
+ constructor(state, vdfService, wallet, config) {
47
+ this.#state = state;
48
+ this.#vdfService = vdfService;
49
+ this.#wallet = wallet;
50
+ this.#config = config;
51
+ }
52
+
53
+ /**
54
+ * Calculates a proof using the challenge and signed VDF parameters from the round.
55
+ *
56
+ * @param {Buffer} challenge Encoded network, parent, proposer and VDF parameters.
57
+ * @param {number} difficulty VDF iteration count.
58
+ * @param {number} discriminantSizeBits Discriminant size in bits.
59
+ * @returns {Promise<EpochVdfProof>} Calculated proof and the parameters used to create it.
60
+ * @throws {Error} When the worker fails or closes.
61
+ */
62
+ async calculateVDF(challenge, difficulty, discriminantSizeBits) {
63
+ const { result: vdfProof, error } = await this.#vdfService.calculateVDF(
64
+ challenge,
65
+ difficulty,
66
+ discriminantSizeBits,
67
+ );
68
+ if (error) throw new Error(error);
69
+ return { solution: vdfProof.solution, difficulty, discriminantSizeBits };
70
+ }
71
+
72
+ /**
73
+ * Builds and signs a proposal for the epoch after parentEpoch. Each message gets a new session ID.
74
+ *
75
+ * @param {bigint} parentEpoch Signed parent epoch number.
76
+ * @param {Buffer} parentEpochHash Hash of the signed parent epoch.
77
+ * @param {EpochVdfProof} vdfProof Local calculation result.
78
+ * @returns {Promise<object>} Consensus message containing proof_proposal.
79
+ */
80
+ async createProofProposal(parentEpoch, parentEpochHash, vdfProof) {
81
+ const targetEpoch = parentEpoch + 1n;
82
+ const encodedDifficulty = uint32ToBuffer(vdfProof.difficulty);
83
+ const encodedDiscriminantSize = uint16ToBuffer(vdfProof.discriminantSizeBits);
84
+ return consensusMessageFactory(this.#wallet, this.#config)
85
+ .buildProofProposal(
86
+ generateUUID(),
87
+ this.#config.networkId,
88
+ targetEpoch,
89
+ parentEpochHash,
90
+ this.#wallet.address,
91
+ encodedDifficulty,
92
+ encodedDiscriminantSize,
93
+ vdfProof.solution
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Sends a proof approval request through an authenticated consensus connection.
99
+ *
100
+ * @param {string} approverPublicKeyHex Remote wallet public key, encoded as hex.
101
+ * @param {object} proofProposalMessage Signed proof proposal message.
102
+ * @param {import('../../network/services/IndexerConnectionManager.js').default} indexerConnectionManager Peer session manager.
103
+ * @returns {Promise<{resultCode: number|undefined, signature: Buffer|undefined}>} Result code and signature from the protocol reply.
104
+ */
105
+ async sendProofApprovalRequest(approverPublicKeyHex, proofProposalMessage, indexerConnectionManager) {
106
+ if (!indexerConnectionManager.connected(approverPublicKeyHex)) throw new Error('Indexer not in the manager');
107
+
108
+ const approvalResponse = await indexerConnectionManager.send(approverPublicKeyHex, proofProposalMessage);
109
+ return {
110
+ resultCode: approvalResponse?.resultCode,
111
+ signature: approvalResponse?.approval?.approval_sig,
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Finds the wallet identity for an indexer and requests its proof signature. Checks whether the
117
+ * collection is closed before sending. The round checks the collection again when the reply
118
+ * arrives.
119
+ *
120
+ * @param {{key: Buffer}} approverIndexer External indexer from the attempt membership.
121
+ * @param {{currentEpoch: bigint, currentEpochHash: Buffer, vdf: EpochVdfProof}} proofContext Parent and local proof.
122
+ * @param {import('../../network/services/IndexerConnectionManager.js').default} indexerConnectionManager Authenticated consensus sessions.
123
+ * @param {EpochApprovalCollection|null} [approvalCollection=null] Approval collection. A closed collection stops the request before sending.
124
+ * @returns {Promise<EpochProofApproval>} Approval data to be added by the round.
125
+ * @throws {Error} If identity lookup, request delivery or approval validation fails.
126
+ */
127
+ async requestProofApproval(approverIndexer, {currentEpoch, currentEpochHash, vdf: vdfProof}, indexerConnectionManager, approvalCollection = null) {
128
+ // Each peer request needs its own RPC session ID; the signed proposal content stays the same.
129
+ const proofProposalMessage = await this.createProofProposal(currentEpoch, currentEpochHash, vdfProof);
130
+ const writerKeyHex = toHex(approverIndexer.key);
131
+ const approverAddressBuffer = await this.#state.getRegisteredWriterKey(writerKeyHex);
132
+ if (!approverAddressBuffer) throw new Error('Registered writer key not found');
133
+
134
+ const approverAddress = addressUtils.bufferToAddress(approverAddressBuffer, this.#config.addressPrefix);
135
+ if (!approverAddress) throw new Error('Writer address could not be decoded');
136
+
137
+ const approverPublicKey = tracCryptoApi.address.decode(approverAddress);
138
+ if (!approverPublicKey) throw new Error('Writer public key could not be decoded');
139
+
140
+ const approverPublicKeyHex = toHex(approverPublicKey);
141
+ if (approvalCollection?.closed) throw new Error('Epoch approval request was cancelled');
142
+
143
+ const { resultCode, signature: approvalSignature } = await this.sendProofApprovalRequest(approverPublicKeyHex, proofProposalMessage, indexerConnectionManager);
144
+
145
+ if (resultCode !== ConsensusResultCode.OK) {
146
+ const approvalError = new Error(`Approval rejected with result code ${resultCode}`);
147
+ approvalError.resultCode = resultCode;
148
+ throw approvalError;
149
+ }
150
+
151
+ if (!approvalSignature) throw new Error('Approval signature not received');
152
+
153
+ return {
154
+ signature: approvalSignature,
155
+ approver: approverAddressBuffer
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Encodes a SET_EPOCH operation from the local proposal and external approvals.
161
+ *
162
+ * @param {object} proofProposal Signed protocol proof fields.
163
+ * @param {EpochProofApproval[]} proofApprovals Signatures from other indexers. proofProposal contains the proposer signature.
164
+ * @returns {Promise<Buffer>} Encoded SET_EPOCH operation ready for appender selection.
165
+ */
166
+ async buildSetEpochPayload(proofProposal, proofApprovals) {
167
+ const encodedProofProposal = encodeProofProposal({
168
+ network_id: proofProposal.network_id,
169
+ epoch: proofProposal.epoch,
170
+ previous_epoch_record_hash: proofProposal.previous_epoch_record_hash,
171
+ proposer: proofProposal.proposer,
172
+ difficulty: proofProposal.difficulty,
173
+ discriminant_bit_size: proofProposal.discriminant_bit_size,
174
+ proof: proofProposal.proof,
175
+ signature: proofProposal.signature,
176
+ });
177
+ const encodedApprovals = proofApprovals.map(({ signature, approver }) =>
178
+ encodeProofProposalApproval({
179
+ approver,
180
+ approval_sig: signature,
181
+ })
182
+ );
183
+ const message = await applyStateMessageFactory(this.#wallet, this.#config)
184
+ .buildCompleteSetEpochMessage(this.#wallet.address, encodedProofProposal, encodedApprovals);
185
+ return encodeApplyOperation(message);
186
+ }
187
+
188
+ /**
189
+ * Queues the epoch append. Calls authorizeEpochAppend just before Autobase.append. Completing the
190
+ * local write does not confirm the epoch. The round still checks its signed result.
191
+ *
192
+ * @param {Buffer} epochPayload Exact bytes reserved by the appender selection.
193
+ * @param {function({writerKey: string, from: number}): Promise<void>} authorizeEpochAppend Checks the selection and saves the append details locally.
194
+ * @returns {Promise<void>} Result returned by the write queue.
195
+ */
196
+ appendSetEpoch(epochPayload, authorizeEpochAppend) {
197
+ return this.#state.appendEpoch(epochPayload, authorizeEpochAppend);
198
+ }
199
+
200
+ /**
201
+ * Returns all indexers except the local writer. An admin who is also an indexer can approve the
202
+ * proof.
203
+ *
204
+ * @param {Array<{key: Buffer}>} indexers Indexer list read for this attempt.
205
+ * @returns {Array<{key: Buffer}>} Other indexers that can approve the proof.
206
+ */
207
+ getProofApprovers(indexers) {
208
+ const writingKey = this.#state.writingKey;
209
+ return indexers.filter(({ key: writerKey }) => !b4a.equals(writerKey, writingKey));
210
+ }
211
+ }
@@ -0,0 +1,335 @@
1
+ import SchedulableService from '../../../utils/scheduler/SchedulableService.js';
2
+ import { Logger } from '../../../utils/logger.js';
3
+ import { CustomEventType } from '../../../utils/constants.js';
4
+ import { EpochCoordinationRound } from './EpochCoordinationRound.js';
5
+ import { VDFServiceManager } from './VDFServiceManager.js';
6
+
7
+ /**
8
+ * Schedules epoch rounds on this indexer. When consensus config changes, cancels the current round,
9
+ * replaces the VDF worker and resumes scheduling. All rounds use the same appender selector.
10
+ */
11
+ class EpochCoordinatorService extends SchedulableService {
12
+ #state;
13
+ #wallet;
14
+ #config;
15
+ #indexerConnectionManager;
16
+ #roundIntervalMs;
17
+ #logger;
18
+ #vdfServiceManager;
19
+ #coordinationEnabled = false;
20
+ #currentRound = null;
21
+ #pendingRoundExecutions = new Set();
22
+ #configReloadPromise = null;
23
+ #cancelReloadDelay = null;
24
+ #consensusConfigChangeListener;
25
+ #appenderSelector;
26
+
27
+ /**
28
+ * Creates the scheduler and VDF service manager. Call start() to enable epoch rounds.
29
+ *
30
+ * @param {import('../../state/State.js').default} state Signed ledger access and consensus events.
31
+ * @param {object} wallet Wallet used to sign local proofs.
32
+ * @param {object} config Round intervals, timeouts and network configuration.
33
+ * @param {import('../../network/services/IndexerConnectionManager.js').default} indexerConnectionManager Authenticated peer sessions.
34
+ * @param {import('../epochAppenderSelection/EpochAppenderSelector.js').EpochAppenderSelector|null} [appenderSelector=null] Shared selector required once genesis is initialized.
35
+ */
36
+ constructor(state, wallet, config, indexerConnectionManager, appenderSelector = null) {
37
+ super();
38
+ this.#state = state;
39
+ this.#wallet = wallet;
40
+ this.#config = config;
41
+ this.#indexerConnectionManager = indexerConnectionManager;
42
+ this.#appenderSelector = appenderSelector;
43
+ this.#roundIntervalMs = config.epochInterval;
44
+ this.#logger = new Logger(config);
45
+ this.#vdfServiceManager = new VDFServiceManager(state, wallet, config);
46
+ this.#consensusConfigChangeListener = this.#handleConfigChange.bind(this);
47
+ }
48
+
49
+ /**
50
+ * Opens the VDF service and starts listening for consensus config changes.
51
+ *
52
+ * @returns {Promise<void>}
53
+ */
54
+ async _open() {
55
+ await this.#vdfServiceManager.open();
56
+ this.#state.on(CustomEventType.CONSENSUS_CONFIG_CHANGED, this.#consensusConfigChangeListener);
57
+ }
58
+
59
+ /**
60
+ * Removes the config listener, cancels the current round and closes the VDF service. Does not wait
61
+ * for network operations that cannot be cancelled.
62
+ *
63
+ * @returns {Promise<void>}
64
+ */
65
+ async _close() {
66
+ this.#state.off(CustomEventType.CONSENSUS_CONFIG_CHANGED, this.#consensusConfigChangeListener);
67
+ this.#coordinationEnabled = false;
68
+ this.#cancelReloadDelay?.();
69
+
70
+ const roundCancelled = this.#cancelCurrentRound();
71
+ const schedulerStopped = super.stop(false);
72
+ const vdfClosed = this.#vdfServiceManager.close();
73
+
74
+ await Promise.all([
75
+ roundCancelled,
76
+ schedulerStopped,
77
+ this.#configReloadPromise,
78
+ vdfClosed,
79
+ ]);
80
+ }
81
+
82
+ /**
83
+ * Opens the VDF service if needed and starts the scheduler.
84
+ *
85
+ * @param {number} initialDelayMs delay before the first coordination round
86
+ * @returns {Promise<boolean>} true when the scheduler was started
87
+ */
88
+ async start(initialDelayMs = this.#roundIntervalMs) {
89
+ if (this.isSchedulerRunning || this.closing !== null || this.closed) return false;
90
+
91
+ this.#coordinationEnabled = true;
92
+
93
+ try {
94
+ await this.#openVdf();
95
+ } catch (error) {
96
+ this.#logger.error(`[EpochCoordinatorService] failed to initialize VDF service: ${error.message}`);
97
+ return false;
98
+ }
99
+
100
+ if (!this.#canStart()) return false;
101
+ return super.start(initialDelayMs);
102
+ }
103
+
104
+ /**
105
+ * Stops the scheduler and cancels the current round.
106
+ *
107
+ * @param {boolean} waitForCurrent wait for operations which have already started
108
+ * @returns {Promise<boolean>} true when a running scheduler was stopped
109
+ */
110
+ async stop(waitForCurrent = true) {
111
+ this.#coordinationEnabled = false;
112
+ this.#cancelReloadDelay?.();
113
+ const roundCancelled = this.#cancelCurrentRound();
114
+ const schedulerStopped = await super.stop(false);
115
+
116
+ await roundCancelled;
117
+ if (waitForCurrent) {
118
+ await Promise.allSettled([...this.#pendingRoundExecutions]);
119
+ }
120
+
121
+ return schedulerStopped;
122
+ }
123
+
124
+ /** @returns {number} delay between coordination rounds */
125
+ getScheduleInterval() {
126
+ return this.#roundIntervalMs;
127
+ }
128
+
129
+ /**
130
+ * Starts one round and lets it schedule the next run. Only the current round may set the next
131
+ * timer. A result from an older round cannot replace it.
132
+ *
133
+ * @param {function(number): void} scheduleNextRound Schedules the next round after the given delay in milliseconds.
134
+ * @param {function(): void} holdScheduling Prevent the scheduler from scheduling automatically.
135
+ * @returns {Promise<void>} Completes when run() returns. The round may still be active.
136
+ */
137
+ async worker(scheduleNextRound, holdScheduling) {
138
+ if (!this.#canRun()) return;
139
+
140
+ const epochOperations = this.#vdfServiceManager.operations;
141
+ if (!epochOperations) return;
142
+
143
+ holdScheduling();
144
+
145
+ const round = new EpochCoordinationRound({
146
+ state: this.#state,
147
+ wallet: this.#wallet,
148
+ config: this.#config,
149
+ manager: this.#indexerConnectionManager,
150
+ logger: this.#logger,
151
+ operations: epochOperations,
152
+ intervalMs: this.#roundIntervalMs,
153
+ appenderSelector: this.#appenderSelector,
154
+ });
155
+
156
+ this.#replaceCurrentRound(round);
157
+
158
+ const roundExecution = round.run((delayMs) => this.#scheduleNextRound(round, scheduleNextRound, delayMs));
159
+ this.#pendingRoundExecutions.add(roundExecution);
160
+
161
+ try {
162
+ await roundExecution;
163
+ } catch (error) {
164
+ if (round !== this.#currentRound || !this.#canRun()) return;
165
+
166
+ this.#logger.error(`[EpochCoordinatorService] round failed: ${error.message}`);
167
+ await this.#cancelRound(round);
168
+ this.#scheduleNextRound(round, scheduleNextRound, this.#roundIntervalMs);
169
+ } finally {
170
+ this.#pendingRoundExecutions.delete(roundExecution);
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Waits for any config reload and opens the VDF service before scheduling starts. Repeats if
176
+ * another reload starts while the service is opening.
177
+ *
178
+ * @returns {Promise<void>}
179
+ */
180
+ async #openVdf() {
181
+ do {
182
+ if (this.#configReloadPromise) await this.#configReloadPromise;
183
+ await this.#vdfServiceManager.open();
184
+ } while (this.#configReloadPromise);
185
+ }
186
+
187
+ /**
188
+ * Checks whether the scheduler can run or schedule another round.
189
+ *
190
+ * @returns {boolean}
191
+ */
192
+ #canRun() {
193
+ return this.#canStart() && !this.isInterrupted;
194
+ }
195
+
196
+ /**
197
+ * Checks whether coordination is enabled and the service can start. A temporary scheduler stop
198
+ * during config reload does not disable coordination.
199
+ *
200
+ * @returns {boolean}
201
+ */
202
+ #canStart() {
203
+ return this.#coordinationEnabled && this.closing === null && !this.closed;
204
+ }
205
+
206
+ /**
207
+ * Sets the current round and cancels the previous one.
208
+ *
209
+ * @param {EpochCoordinationRound} round New round to use for coordination.
210
+ */
211
+ #replaceCurrentRound(round) {
212
+ const previousRound = this.#currentRound;
213
+ this.#currentRound = round;
214
+
215
+ if (previousRound) void this.#cancelRound(previousRound);
216
+ }
217
+
218
+ /**
219
+ * Clears the current round and waits for its cancellation.
220
+ *
221
+ * @returns {Promise<void>}
222
+ */
223
+ async #cancelCurrentRound() {
224
+ const round = this.#currentRound;
225
+ this.#currentRound = null;
226
+
227
+ if (round) await this.#cancelRound(round);
228
+ }
229
+
230
+ /**
231
+ * Cancels a round and logs any cleanup error.
232
+ *
233
+ * @param {EpochCoordinationRound} round Round to cancel.
234
+ * @returns {Promise<void>}
235
+ */
236
+ async #cancelRound(round) {
237
+ try {
238
+ await round.cancel();
239
+ } catch (error) {
240
+ this.#logger.error(`[EpochCoordinatorService] failed to cancel epoch round: ${error.message}`);
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Schedules the next run if this round is still current. A callback from an older round cannot
246
+ * change the timer.
247
+ *
248
+ * @param {EpochCoordinationRound} round Round requesting the next run.
249
+ * @param {(delay: number) => void} scheduleNextRound Callback that sets the next scheduler timer.
250
+ * @param {number} delayMs Delay before the next round, in milliseconds.
251
+ */
252
+ #scheduleNextRound(round, scheduleNextRound, delayMs) {
253
+ if (round !== this.#currentRound || !this.#canRun()) return;
254
+
255
+ this.#currentRound = null;
256
+ scheduleNextRound(delayMs);
257
+ }
258
+
259
+ /**
260
+ * Starts a consensus config reload. Events received during the same reload are ignored.
261
+ *
262
+ * @returns {void}
263
+ */
264
+ #handleConfigChange() {
265
+ if (this.closing !== null || this.closed || this.#configReloadPromise) return;
266
+ this.#configReloadPromise = this.#reloadConfig();
267
+ }
268
+
269
+ /**
270
+ * Cancels the round, stops the scheduler and reloads VDF. Restarts scheduling if coordination is
271
+ * still enabled.
272
+ *
273
+ * @returns {Promise<void>}
274
+ */
275
+ async #reloadConfig() {
276
+ const shouldRestart = this.#coordinationEnabled;
277
+
278
+ try {
279
+ await Promise.all([
280
+ this.#cancelCurrentRound(),
281
+ super.stop(false),
282
+ shouldRestart ? this.#reloadVdf() : this.#vdfServiceManager.close(),
283
+ ]);
284
+ } catch (error) {
285
+ this.#logger.error(`[EpochCoordinatorService] config reload failed: ${error.message}`);
286
+ return;
287
+ } finally {
288
+ this.#configReloadPromise = null;
289
+ }
290
+
291
+ if (!shouldRestart) return;
292
+ if (!this.#vdfServiceManager.operations || !this.#canStart()) return;
293
+
294
+ super.start(0);
295
+ }
296
+
297
+ /** Reloads VDF and retries on failure while coordination remains enabled. */
298
+ async #reloadVdf() {
299
+ while (this.#canStart()) {
300
+ try {
301
+ await this.#vdfServiceManager.replace();
302
+ return true;
303
+ } catch (error) {
304
+ if (!this.#canStart()) return false;
305
+
306
+ this.#logger.error(
307
+ `[EpochCoordinatorService] config reload failed: ${error.message}; ` +
308
+ `retrying in ${this.#config.epochBackoffDelay}ms`,
309
+ );
310
+ await this.#waitBeforeVdfReloadRetry();
311
+ }
312
+ }
313
+
314
+ return false;
315
+ }
316
+
317
+ /**
318
+ * Waits before retrying a VDF reload. stop() and close() end the wait early.
319
+ *
320
+ * @returns {Promise<void>} Resolves when the timer ends or the wait is cancelled.
321
+ */
322
+ #waitBeforeVdfReloadRetry() {
323
+ return new Promise(resolve => {
324
+ const timer = setTimeout(resolve, this.#config.epochBackoffDelay);
325
+ this.#cancelReloadDelay = () => {
326
+ clearTimeout(timer);
327
+ resolve();
328
+ };
329
+ }).finally(() => {
330
+ this.#cancelReloadDelay = null;
331
+ });
332
+ }
333
+ }
334
+
335
+ export default EpochCoordinatorService;
@@ -0,0 +1,116 @@
1
+ import b4a from 'b4a';
2
+ import { CustomEventType, EPOCH_BYTE_LENGTH } from '../../../utils/constants.js';
3
+ import { EPOCH_EVENTS, EPOCH_STATES } from './EpochStateMachine.js';
4
+
5
+ /**
6
+ * Adds an event listener and returns a function that removes it.
7
+ *
8
+ * @param {object} eventEmitter Event source with on and off methods.
9
+ * @param {string} event Event name.
10
+ * @param {Function} handler Listener called by the source.
11
+ * @returns {function(): void} Removes this listener.
12
+ */
13
+ const listenTo = (eventEmitter, event, handler) => {
14
+ eventEmitter.on(event, handler);
15
+ return () => eventEmitter.off(event, handler);
16
+ };
17
+
18
+ /**
19
+ * Listens for validated proposals and epoch events during one round. Manages the approval timeout and
20
+ * asks the round to check signed epoch progress. Closing the round's FSM removes the listeners and
21
+ * timers.
22
+ */
23
+ export class EpochRoundListeners {
24
+ #state;
25
+ #epochStateMachine;
26
+ #config;
27
+ #isRoundActive;
28
+ #listenerCleanups = [];
29
+ #checkSignedEpochProgress;
30
+
31
+ /**
32
+ * Sets the event sources and callbacks for this round. start() adds the listeners.
33
+ *
34
+ * @param {object} dependencies Services and callbacks used by this round.
35
+ * @param {object} dependencies.state Ledger and consensus event emitter.
36
+ * @param {import('./EpochStateMachine.js').EpochStateMachine} dependencies.machine Round FSM.
37
+ * @param {object} dependencies.config Supplies epochSignatureTimeout in milliseconds.
38
+ * @param {function(): boolean} dependencies.isRoundActive Rejects late events after cancellation or completion.
39
+ * @param {function(): Promise<void>} dependencies.onEpochProgress Checks signed progress after an epoch event.
40
+ */
41
+ constructor({ state, machine: epochStateMachine, config, isRoundActive, onEpochProgress: checkSignedEpochProgress }) {
42
+ this.#state = state;
43
+ this.#epochStateMachine = epochStateMachine;
44
+ this.#config = config;
45
+ this.#isRoundActive = isRoundActive;
46
+ this.#checkSignedEpochProgress = checkSignedEpochProgress;
47
+ }
48
+
49
+ /** Starts this round's listeners and timers. They are removed when the FSM closes. */
50
+ start() {
51
+ this.#listenForValidatedProposals();
52
+ this.#listenForEpochCreated();
53
+ this.#manageApprovalDeadline();
54
+
55
+ this.#epochStateMachine.once('close', () => this.#removeListenersAndTimers());
56
+ }
57
+
58
+ /** Removes the listeners and timers added by start(). */
59
+ #removeListenersAndTimers() {
60
+ for (const cleanup of this.#listenerCleanups.splice(0)) {
61
+ cleanup();
62
+ }
63
+ }
64
+
65
+ /** Marks a remote proposal only if it matches this round's epoch and parent hash. */
66
+ #listenForValidatedProposals() {
67
+ this.#listenerCleanups.push(
68
+ listenTo(this.#state, CustomEventType.EPOCH_PROPOSAL_VALIDATION_SUCCESS, (event) => {
69
+ if (!this.#isRoundActive()) return;
70
+ const { currentEpoch, currentEpochHash } = this.#epochStateMachine.context;
71
+ const { epoch, previous_epoch_record_hash: proposalParentHash } = event?.proofProposal ?? {};
72
+ // Validation can finish after the round that received the proposal has ended.
73
+ if (typeof currentEpoch !== 'bigint') return;
74
+ if (!b4a.isBuffer(epoch) || epoch.length !== EPOCH_BYTE_LENGTH) return;
75
+ if (epoch.readBigUInt64BE(0) !== currentEpoch + 1n) return;
76
+ if (!b4a.isBuffer(currentEpochHash) || !b4a.isBuffer(proposalParentHash)) return;
77
+ if (!b4a.equals(proposalParentHash, currentEpochHash)) return;
78
+ this.#epochStateMachine.appendContext({ remoteProposalReceived: true });
79
+ }),
80
+ );
81
+ }
82
+
83
+ /** Asks the round to check signed progress when an epoch event arrives. */
84
+ #listenForEpochCreated() {
85
+ this.#listenerCleanups.push(
86
+ listenTo(this.#state, CustomEventType.EPOCH_CREATED, async () => {
87
+ if (!this.#isRoundActive()) return;
88
+ await this.#checkSignedEpochProgress();
89
+ }),
90
+ );
91
+ }
92
+
93
+ /** Starts the approval timer when collection begins and clears it when collection ends. */
94
+ #manageApprovalDeadline() {
95
+ let approvalDeadlineTimer = null;
96
+
97
+ this.#epochStateMachine.on('*', ({ next, prev }) => {
98
+ if (!this.#isRoundActive()) return;
99
+
100
+ if (next === EPOCH_STATES.COLLECT_APPROVALS && prev !== EPOCH_STATES.COLLECT_APPROVALS) {
101
+ approvalDeadlineTimer = setTimeout(
102
+ () => {
103
+ if (this.#isRoundActive()) {
104
+ this.#epochStateMachine.send(EPOCH_EVENTS.APPROVAL_COLLECTION_FAILED);
105
+ }
106
+ },
107
+ this.#config.epochSignatureTimeout,
108
+ );
109
+ } else if (prev === EPOCH_STATES.COLLECT_APPROVALS && next !== EPOCH_STATES.COLLECT_APPROVALS) {
110
+ clearTimeout(approvalDeadlineTimer);
111
+ }
112
+ });
113
+
114
+ this.#listenerCleanups.push(() => clearTimeout(approvalDeadlineTimer));
115
+ }
116
+ }