trac-msb 0.2.26 → 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 (136) 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 +41 -7
  23. package/src/config/env.js +33 -9
  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/State.js +1789 -173
  102. package/src/core/state/StateEventQueue.js +69 -0
  103. package/src/core/state/utils/address.js +10 -6
  104. package/src/core/state/utils/epochProof.js +109 -0
  105. package/src/core/state/utils/escrowEntry.js +178 -0
  106. package/src/core/state/validators/StateValidationSchema.js +757 -0
  107. package/src/core/state/validators/stateValidationRules.js +312 -0
  108. package/src/index.js +255 -65
  109. package/src/messages/consensus/v1/ConsensusMessageBuilder.js +368 -0
  110. package/src/messages/consensus/v1/ConsensusMessageDirector.js +142 -0
  111. package/src/messages/consensus/v1/consensusMessageFactory.js +12 -0
  112. package/src/messages/consensus/v1/epochAppendMessage.js +58 -0
  113. package/src/messages/network/v1/NetworkMessageBuilder.js +15 -7
  114. package/src/messages/state/ApplyStateMessageBuilder.js +499 -13
  115. package/src/messages/state/ApplyStateMessageDirector.js +261 -2
  116. package/src/utils/StateMachine.js +125 -0
  117. package/src/utils/applyOperations.js +45 -2
  118. package/src/utils/buffer.js +239 -59
  119. package/src/utils/consensusConfig.js +76 -0
  120. package/src/utils/constants.js +174 -73
  121. package/src/utils/helpers.js +0 -25
  122. package/src/utils/htlcLock.js +122 -0
  123. package/src/utils/logger.js +10 -4
  124. package/src/utils/migrationUtils.js +2 -1
  125. package/src/utils/normalizers.js +113 -0
  126. package/src/utils/scheduler/SchedulableService.js +107 -0
  127. package/src/utils/{Scheduler.js → scheduler/Scheduler.js} +56 -17
  128. package/src/core/network/services/ConnectionManager.js +0 -406
  129. package/src/utils/check.js +0 -525
  130. package/src/utils/protobuf/applyOperations.cjs +0 -1373
  131. package/src/utils/protobuf/operationHelpers.js +0 -81
  132. /package/src/core/network/protocols/{legacy → validators/legacy}/handlers/LegacyResponseHandler.js +0 -0
  133. /package/src/core/network/protocols/{legacy → validators/legacy}/validators/ValidatorResponse.js +0 -0
  134. /package/src/core/network/protocols/{legacy → validators/legacy}/validators/base/BaseResponse.js +0 -0
  135. /package/src/core/network/protocols/{v1 → validators/v1}/validators/V1LivenessRequest.js +0 -0
  136. /package/src/core/network/protocols/{v1 → validators/v1}/validators/V1LivenessResponse.js +0 -0
@@ -0,0 +1,72 @@
1
+ import Protomux from 'protomux';
2
+ import _ from 'lodash';
3
+ import ConsensusV1MessageRouter from "./v1/ConsensusV1MessageRouter.js";
4
+ import ConsensusV1Protocol from "./v1/ConsensusV1Protocol.js";
5
+ import ConsensusV1ConnectionPolicy from './v1/ConsensusV1ConnectionPolicy.js';
6
+ import { Logger } from '../../../../utils/logger.js';
7
+
8
+ const PROTOCOL = 'consensus/v1';
9
+
10
+ class IndexerMessages {
11
+ #consensusRouter;
12
+ #pendingRequestService;
13
+ #onSessionClosed;
14
+ #connectionPolicy;
15
+
16
+ constructor(state, wallet, config, pendingRequestService, onSessionClosed, appenderSelector = null) {
17
+ this.#pendingRequestService = pendingRequestService;
18
+ this.#onSessionClosed = onSessionClosed;
19
+ const logger = new Logger(config);
20
+ this.#connectionPolicy = new ConsensusV1ConnectionPolicy(state, logger, config);
21
+ this.#consensusRouter = new ConsensusV1MessageRouter(
22
+ state, wallet, config, pendingRequestService, this.#connectionPolicy, appenderSelector
23
+ );
24
+ }
25
+
26
+ createProtocolSession(connection) {
27
+ return new ConsensusV1Protocol(
28
+ this.#consensusRouter,
29
+ connection,
30
+ this.#pendingRequestService,
31
+ session => {
32
+ if (connection.protocolSessions?.indexer !== session) return;
33
+ delete connection.protocolSessions.indexer;
34
+ this.#onSessionClosed(connection);
35
+ },
36
+ this.#connectionPolicy
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Opens the consensus/v1 channel only for a peer recognized as an indexer.
42
+ * The close callback removes the session reference so a new channel can open.
43
+ */
44
+ async attachChannel(connection) {
45
+ if (connection.destroyed) return;
46
+ if (_.isNil(connection.protocolSessions)) {
47
+ connection.protocolSessions = {};
48
+ }
49
+ if (connection.protocolSessions.indexer) return;
50
+ try {
51
+ if (!await this.#connectionPolicy.shouldAcceptConsensusChannel(connection)) return;
52
+ // The connection may close or another attempt may create the session while awaiting state.
53
+ if (connection.destroyed || connection.protocolSessions.indexer) return;
54
+ const session = this.createProtocolSession(connection);
55
+ if (!session.closed) connection.protocolSessions.indexer = session;
56
+ } catch (error) {
57
+ // A rejected Protomux pairing callback would destroy the shared transport.
58
+ this.#connectionPolicy.handleLocalError(`IndexerMessages: failed to open Consensus V1 channel: ${error.message}`);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Applies the same admission check to remote opens. Returning without creating
64
+ * a session rejects only this channel attempt. Replication and later retries remain possible.
65
+ */
66
+ prepareConnection(connection) {
67
+ const mux = Protomux.from(connection);
68
+ mux.pair({ protocol: PROTOCOL }, () => this.attachChannel(connection));
69
+ }
70
+ }
71
+
72
+ export default IndexerMessages;
@@ -0,0 +1,57 @@
1
+ import { ConsensusResultCode, CustomEventType } from '../../../../../utils/constants.js';
2
+ import { publicKeyToAddress } from '../../../../../utils/helpers.js';
3
+ import { V1ConsensusProtocolError } from './V1ConsensusProtocolError.js';
4
+
5
+ class ConsensusV1ConnectionPolicy {
6
+ #state;
7
+ #logger;
8
+ #config;
9
+
10
+ constructor(state, logger, config) {
11
+ this.#state = state;
12
+ this.#logger = logger;
13
+ this.#config = config;
14
+ }
15
+
16
+ async shouldAcceptConsensusChannel(connection) {
17
+ try {
18
+ const address = publicKeyToAddress(connection.remotePublicKey, this.#config);
19
+ // Waiting for replication inside Protomux pairing can block the shared stream.
20
+ return await this.#state.isIndexerAddress(address, { wait: false, update: false });
21
+ } catch (error) {
22
+ this.handleLocalError(`ConsensusV1ConnectionPolicy: failed to check indexer membership: ${error.message}`);
23
+ return false;
24
+ }
25
+ }
26
+
27
+ // Remote rejections are returned as results, not local validation errors.
28
+ // Returns true when a ban was requested, so the handler skips the response.
29
+ requestPeerBan(connection, error) {
30
+ if (!(error instanceof V1ConsensusProtocolError)) return false;
31
+ if (error.resultCode !== ConsensusResultCode.PUBLIC_KEY_MISMATCH) return false;
32
+
33
+ this.#state.emit(CustomEventType.CONSENSUS_PEER_BAN_REQUESTED, { connection, error });
34
+ return true;
35
+ }
36
+
37
+ // Expired requests and responses from an older session do not penalize the peer.
38
+ shouldIgnoreApproval(pendingEntry, protocolSession) {
39
+ if (!pendingEntry) return true;
40
+ if (!pendingEntry.session) return false;
41
+ return pendingEntry.session !== protocolSession;
42
+ }
43
+
44
+ // Invalid frames, message types, session IDs and unexpected senders close only consensus.
45
+ // Proposal payload rejections use signed responses rather than this routing policy.
46
+ handleInvalidMessage(protocolSession, reason) {
47
+ this.#logger.error(reason);
48
+ protocolSession.close();
49
+ }
50
+
51
+ // Local failures provide no grounds to close a peer's channel or transport.
52
+ handleLocalError(reason) {
53
+ this.#logger.error(reason);
54
+ }
55
+ }
56
+
57
+ export default ConsensusV1ConnectionPolicy;
@@ -0,0 +1,147 @@
1
+ import { decodeConsensusMessage } from '../../../../../codecs/consensus/v1/consensusV1OperationCodec.js'
2
+ import b4a from 'b4a'
3
+ import { ConsensusOperationType, ConsensusResultCode, CONSENSUS_MESSAGE_MAX_BYTE_SIZE } from '../../../../../utils/constants.js'
4
+ import { publicKeyToAddress } from '../../../../../utils/helpers.js'
5
+ import V1EpochProofProposalOperationHandler from './handlers/V1EpochProofProposalOperationHandler.js'
6
+ import V1EpochAppendOperationHandler from './handlers/V1EpochAppendOperationHandler.js';
7
+ import { V1ConsensusProtocolError } from './V1ConsensusProtocolError.js';
8
+
9
+ class ConsensusV1MessageRouter {
10
+ #config
11
+ #connectionPolicy
12
+ #epochProofProposalHandler
13
+ #pendingRequestService
14
+ #appendHandler;
15
+ constructor(
16
+ state,
17
+ wallet,
18
+ config,
19
+ pendingRequestService,
20
+ connectionPolicy,
21
+ appenderSelector = null
22
+ ) {
23
+ this.#config = config
24
+ this.#connectionPolicy = connectionPolicy;
25
+ this.#epochProofProposalHandler = new V1EpochProofProposalOperationHandler(
26
+ state,
27
+ wallet,
28
+ config,
29
+ connectionPolicy
30
+ );
31
+ this.#pendingRequestService = pendingRequestService;
32
+ this.#appendHandler = new V1EpochAppendOperationHandler(state, wallet, config, appenderSelector);
33
+ }
34
+
35
+ async route(incomingMessage, connection, protocolSession) {
36
+ if (protocolSession.closed) return;
37
+
38
+ if (!this.#preValidate(incomingMessage)) {
39
+ this.#handleInvalidMessage(connection, protocolSession, 'Pre-validation failed for incoming Consensus V1 message')
40
+ return;
41
+ }
42
+ let decodedMessage;
43
+
44
+ try {
45
+ decodedMessage = decodeConsensusMessage(incomingMessage)
46
+ } catch (error) {
47
+ this.#handleInvalidMessage(connection, protocolSession, `Failed to decode incoming Consensus V1 message: ${error.message}`)
48
+ return;
49
+ }
50
+
51
+ // again in the next switch statement
52
+ if (!decodedMessage || !Number.isInteger(decodedMessage.type) || decodedMessage.type <= 0) {
53
+ this.#handleInvalidMessage(connection, protocolSession, `Invalid Consensus V1 message type: ${decodedMessage?.type}`)
54
+ return;
55
+ }
56
+
57
+ let pendingRequest;
58
+ try {
59
+ switch (decodedMessage.type) {
60
+ case ConsensusOperationType.REQUEST:
61
+ await this.#appendHandler.handleRequest(decodedMessage, connection, protocolSession);
62
+ break;
63
+ case ConsensusOperationType.REPLY: {
64
+ const entry = this.#pendingRequestService.getPendingRequest(decodedMessage.session_id);
65
+ if (
66
+ !entry || entry.requestType !== ConsensusOperationType.REQUEST ||
67
+ entry.session !== protocolSession ||
68
+ entry.requestedTo !== b4a.toString(connection.remotePublicKey, 'hex')
69
+ ) {
70
+ break;
71
+ }
72
+ pendingRequest = entry;
73
+ const response = await this.#appendHandler.handleReply(
74
+ decodedMessage,
75
+ connection,
76
+ protocolSession,
77
+ entry.appendRequest
78
+ );
79
+ if (
80
+ response && !protocolSession.closed &&
81
+ this.#pendingRequestService.getPendingRequest(decodedMessage.session_id) === entry
82
+ ) {
83
+ this.#pendingRequestService.resolvePendingRequest(decodedMessage.session_id, response);
84
+ }
85
+ break;
86
+ }
87
+ case ConsensusOperationType.PROOF_PROPOSAL:
88
+ await this.#epochProofProposalHandler.handleRequest(decodedMessage, connection, protocolSession);
89
+ break;
90
+ case ConsensusOperationType.PROOF_PROPOSAL_APPROVAL: {
91
+ const pendingEntry = this.#pendingRequestService.getPendingRequest(decodedMessage.session_id)
92
+ if (pendingEntry && pendingEntry.requestType !== ConsensusOperationType.PROOF_PROPOSAL) break;
93
+ const expectedPeer = pendingEntry?.requestedTo ? b4a.from(pendingEntry.requestedTo, 'hex') : null;
94
+ if (expectedPeer && !b4a.equals(expectedPeer, connection.remotePublicKey)) {
95
+ this.#handleInvalidMessage(connection, protocolSession, 'Consensus V1 message: approval received from unexpected peer')
96
+ break;
97
+ }
98
+ if (this.#connectionPolicy.shouldIgnoreApproval(pendingEntry, protocolSession)) break;
99
+
100
+ pendingRequest = pendingEntry;
101
+ const response = await this.#epochProofProposalHandler.handleApproval(
102
+ decodedMessage,
103
+ connection,
104
+ protocolSession,
105
+ pendingEntry.proofProposal
106
+ );
107
+ // Validation may finish after this request was cancelled or replaced.
108
+ if (this.#pendingRequestService.getPendingRequest(decodedMessage.session_id) === pendingEntry) {
109
+ this.#pendingRequestService.resolvePendingRequest(decodedMessage.session_id, response);
110
+ }
111
+ break;
112
+ }
113
+ default:
114
+ this.#handleInvalidMessage(connection, protocolSession, `Unsupported Consensus V1 message type: ${decodedMessage.type}`)
115
+ }
116
+ } catch (error) {
117
+ if (pendingRequest && this.#pendingRequestService.getPendingRequest(decodedMessage.session_id) === pendingRequest) {
118
+ this.#pendingRequestService.rejectPendingRequest(decodedMessage.session_id, error);
119
+ }
120
+ const isArbitrationMessage = decodedMessage.type === ConsensusOperationType.REQUEST ||
121
+ decodedMessage.type === ConsensusOperationType.REPLY;
122
+ if (isArbitrationMessage && this.#connectionPolicy.requestPeerBan(connection, error)) {
123
+ return;
124
+ }
125
+ if (isArbitrationMessage && error instanceof V1ConsensusProtocolError && error.resultCode === ConsensusResultCode.EPOCH_INVALID) {
126
+ return;
127
+ }
128
+ this.#logError(connection, `Unhandled error while routing Consensus V1 message: ${error.message}`)
129
+ }
130
+ }
131
+
132
+ #preValidate(incomingMessage) {
133
+ return !(!incomingMessage || !b4a.isBuffer(incomingMessage) || incomingMessage.length === 0 || incomingMessage.length > CONSENSUS_MESSAGE_MAX_BYTE_SIZE);
134
+ }
135
+
136
+ #handleInvalidMessage(connection, protocolSession, reason) {
137
+ const sender = publicKeyToAddress(connection.remotePublicKey, this.#config);
138
+ this.#connectionPolicy.handleInvalidMessage(protocolSession, `ConsensusV1MessageRouter: ${reason}, sender: ${sender}`);
139
+ }
140
+
141
+ #logError(connection, reason) {
142
+ const sender = publicKeyToAddress(connection.remotePublicKey, this.#config)
143
+ this.#connectionPolicy.handleLocalError(`ConsensusV1MessageRouter: ${reason}, sender: ${sender}`);
144
+ }
145
+ }
146
+
147
+ export default ConsensusV1MessageRouter
@@ -0,0 +1,72 @@
1
+ import Protomux from 'protomux';
2
+ import c from 'compact-encoding';
3
+ import { encodeConsensusMessage } from '../../../../../codecs/consensus/v1/consensusV1OperationCodec.js';
4
+
5
+ class ConsensusV1Protocol {
6
+ #channel;
7
+ #session;
8
+ #router;
9
+ #pendingRequestService;
10
+ #publicKeyHex;
11
+ #connectionPolicy;
12
+
13
+ constructor(router, connection, pendingRequestService, onClose, connectionPolicy) {
14
+ this.#router = router;
15
+ this.#pendingRequestService = pendingRequestService;
16
+ this.#publicKeyHex =connection.remotePublicKey.toString('hex');
17
+ this.#connectionPolicy = connectionPolicy;
18
+ this.#init(connection, onClose);
19
+ }
20
+
21
+ get closed() {
22
+ return !this.#channel || this.#channel.closed;
23
+ }
24
+
25
+ #init(connection, onClose) {
26
+ const mux = Protomux.from(connection);
27
+
28
+ this.#channel = mux.createChannel({
29
+ protocol: 'consensus/v1',
30
+ onopen() {},
31
+ onclose: () => {
32
+ this.#pendingRequestService.rejectPendingRequestsForSession(this, new Error('Consensus session closed before response'));
33
+ onClose(this);
34
+ }
35
+ });
36
+
37
+ if (!this.#channel) return; // connection already destroyed before protocol setup completed
38
+
39
+ this.#session = this.#channel.addMessage({
40
+ encoding: c.raw,
41
+ onmessage: (incomingMessage) => {
42
+ this.#router.route(incomingMessage, connection, this).catch((err) => {
43
+ this.#connectionPolicy.handleLocalError(`ConsensusV1Protocol: unhandled router error for Consensus V1 message, peer: ${this.#publicKeyHex}: ${err.message}`);
44
+ });
45
+ }
46
+ });
47
+ this.#channel.open();
48
+ }
49
+
50
+ async send(message) {
51
+ if (this.closed) throw new Error('Consensus session is closed');
52
+ const encodedMessage = encodeConsensusMessage(message);
53
+ const msgReplyPromise = this.#pendingRequestService.registerPendingRequest(this.#publicKeyHex, message, this);
54
+ try {
55
+ this.#session.send(encodedMessage);
56
+ } catch (error) {
57
+ this.#pendingRequestService.rejectPendingRequest(message.session_id, error);
58
+ }
59
+ return msgReplyPromise;
60
+ }
61
+
62
+ sendAndForget(message) {
63
+ if (this.closed) return;
64
+ this.#session?.send(encodeConsensusMessage(message));
65
+ }
66
+
67
+ close() {
68
+ this.#channel?.close();
69
+ }
70
+ }
71
+
72
+ export default ConsensusV1Protocol;
@@ -0,0 +1,108 @@
1
+ import {ConsensusOperationType, PEER_PUBLIC_KEY_HEX_LENGTH, ConsensusResultCode} from '../../../../../utils/constants.js';
2
+ import {isHexString, publicKeyToAddress} from '../../../../../utils/helpers.js';
3
+ import {V1ConsensusProtocolError} from "./V1ConsensusProtocolError.js";
4
+ import BasePendingRequestService from '../../../shared/BasePendingRequestService.js';
5
+
6
+ export class IndexerPendingRequestServiceTimeoutError extends Error {
7
+ constructor(requestId, peerAddress, timeoutMs) {
8
+ super(`Pending request ${requestId} to peer ${peerAddress} timed out after ${timeoutMs} ms.`);
9
+ this.name = this.constructor.name;
10
+ }
11
+ }
12
+
13
+ export default class IndexerPendingRequestService extends BasePendingRequestService {
14
+ #config;
15
+
16
+ constructor(config) {
17
+ super();
18
+ this.#config = config;
19
+ }
20
+
21
+ #validateRegisterInput(peerPubKeyHex, message) {
22
+ if (!isHexString(peerPubKeyHex) || peerPubKeyHex.length !== PEER_PUBLIC_KEY_HEX_LENGTH) {
23
+ throw new Error('Invalid peer public key. Expected 32-byte hex string.');
24
+ }
25
+
26
+ if (typeof message?.session_id !== 'string' || message?.session_id.length === 0) {
27
+ throw new Error('Pending request ID must be a non-empty string.');
28
+ }
29
+
30
+ if (message.type !== ConsensusOperationType.PROOF_PROPOSAL && message.type !== ConsensusOperationType.REQUEST) {
31
+ throw new Error('Unsupported pending request type');
32
+ }
33
+ }
34
+
35
+ /*
36
+ @returns {Promise}
37
+ */
38
+ registerPendingRequest(peerPubKeyHex, message, session = null) {
39
+ this.#validateRegisterInput(peerPubKeyHex, message);
40
+ const id = message.session_id;
41
+ const peerAddress = publicKeyToAddress(peerPubKeyHex, this.#config);
42
+ if (this._pendingRequests.size >= this.#config.maxPendingRequestsInPendingRequestsService) {
43
+ throw new Error('Maximum number of pending requests reached.');
44
+ }
45
+
46
+ if (this._pendingRequests.has(id)) {
47
+ throw new Error(`Pending request with ID ${id} from peer ${peerPubKeyHex} already exists.`);
48
+ }
49
+
50
+ const entry = {
51
+ id: id,
52
+ requestType: message.type,
53
+ requestedTo: peerPubKeyHex,
54
+ session,
55
+ proofProposal: message.proof_proposal,
56
+ timeoutId: null,
57
+ resolve: null,
58
+ reject: null,
59
+ };
60
+
61
+ if (message.type === ConsensusOperationType.REQUEST) {
62
+ entry.appendRequest = message;
63
+ }
64
+
65
+ const promise = new Promise((resolve, reject) => {
66
+ entry.resolve = resolve;
67
+ entry.reject = reject;
68
+ });
69
+
70
+ entry.timeoutId = setTimeout(() => {
71
+ this.rejectPendingRequest(
72
+ id,
73
+ new IndexerPendingRequestServiceTimeoutError(
74
+ id,
75
+ peerAddress,
76
+ this.#config.indexerPendingRequestTimeout
77
+ )
78
+ );
79
+
80
+ }, this.#config.indexerPendingRequestTimeout);
81
+
82
+ this._pendingRequests.set(id, entry);
83
+ return promise;
84
+ }
85
+
86
+ /** Complete a proof approval or a validated arbitration reply. */
87
+ resolvePendingRequest(id, result = ConsensusResultCode.OK) {
88
+ const entry = this.getAndDeletePendingRequest(id);
89
+ if (!entry) return false;
90
+ entry.resolve(result);
91
+ return true;
92
+ }
93
+
94
+ _createShutdownError(id) {
95
+ return new V1ConsensusProtocolError(
96
+ ConsensusResultCode.UNEXPECTED_ERROR,
97
+ `Pending request ${id} cancelled (shutdown).`
98
+ );
99
+ }
100
+
101
+ rejectPendingRequestsForSession(session, error) {
102
+ for (const [id, entry] of this._pendingRequests) {
103
+ if (entry.session === session) {
104
+ this.rejectPendingRequest(id, error);
105
+ }
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,29 @@
1
+ import {ConsensusResultCode} from '../../../../../utils/constants.js';
2
+
3
+ /**
4
+ * V1 consensus protocol error type.
5
+ *
6
+ * `V1ConsensusProtocolError` is the v1 base class used by handlers/validators to attach:
7
+ * - `resultCode`: a stable `ConsensusResultCode` enum value for programmatic handling
8
+ */
9
+ export class V1ConsensusProtocolError extends Error {
10
+ /**
11
+ * @param {number} resultCode Stable rejection reason (a `ConsensusResultCode` enum value).
12
+ * @param {string} message Human-readable error message.
13
+ */
14
+ constructor(resultCode, message) {
15
+ super(message);
16
+ this.name = this.constructor.name;
17
+ this.resultCode = resultCode;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Returns the consensus result code attached to a protocol error.
23
+ *
24
+ * @param {unknown} err Error-like value to inspect.
25
+ * @returns {number} Attached `ConsensusResultCode`, or `ConsensusResultCode.UNEXPECTED_ERROR`.
26
+ */
27
+ export function getResultCode(err) {
28
+ return err instanceof V1ConsensusProtocolError ? err.resultCode : ConsensusResultCode.UNEXPECTED_ERROR;
29
+ }
@@ -0,0 +1,68 @@
1
+ import V1EpochAppendRequest from '../validators/V1EpochAppendRequest.js';
2
+ import V1EpochAppendReply from '../validators/V1EpochAppendReply.js';
3
+ import { consensusMessageFactory } from '../../../../../../messages/consensus/v1/consensusMessageFactory.js';
4
+ import { sameSelectionContext } from '../../../../../consensus/epochAppenderSelection/epochAppenderSelectionState.js';
5
+
6
+ export default class V1EpochAppendOperationHandler {
7
+ #state;
8
+ #wallet;
9
+ #config;
10
+ #selector;
11
+ #requestValidator;
12
+ #replyValidator;
13
+
14
+ constructor(state, wallet, config, selector) {
15
+ this.#state = state;
16
+ this.#wallet = wallet;
17
+ this.#config = config;
18
+ this.#selector = selector;
19
+ this.#requestValidator = new V1EpochAppendRequest(config, state);
20
+ this.#replyValidator = new V1EpochAppendReply(config, state);
21
+ }
22
+
23
+ /** Validate REQUEST before arbitration; the selector may defer its reply. */
24
+ async handleRequest(message, connection, session) {
25
+ if (!this.#selector || session.closed) {
26
+ return;
27
+ }
28
+ const request = await this.#requestValidator.validate(message, connection);
29
+ if (session.closed) {
30
+ return;
31
+ }
32
+ await this.#selector.refreshAppendOutcome();
33
+ if (!await this.#canReply(request, session)) {
34
+ return;
35
+ }
36
+ await this.#selector.handleSelectionRequest(request, response => {
37
+ return this.#sendReply(message.session_id, response, session);
38
+ });
39
+ }
40
+
41
+ async #canReply(request, session) {
42
+ if (session.closed) {
43
+ return false;
44
+ }
45
+ const context = await this.#state.getEpochAppenderSelectionContext();
46
+ return !session.closed && sameSelectionContext(request, context);
47
+ }
48
+
49
+ async #sendReply(sessionId, request, session) {
50
+ if (session.closed) {
51
+ return;
52
+ }
53
+ const reply = await consensusMessageFactory(this.#wallet, this.#config).buildReply(sessionId, request);
54
+ // Deferred replies can outlive both their epoch and their Protomux session.
55
+ if (await this.#canReply(request, session)) {
56
+ session.sendAndForget(reply);
57
+ }
58
+ }
59
+
60
+ /** @returns {Promise<object|null>} Validated request identity, or null after context/session expiry. */
61
+ async handleReply(message, connection, session, pendingRequest) {
62
+ const request = await this.#replyValidator.validate(message, connection, pendingRequest);
63
+ if (!await this.#canReply(request, session)) {
64
+ return null;
65
+ }
66
+ return request;
67
+ }
68
+ }