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.
- package/SECURITY.md +2 -2
- package/cli/commandHandler.js +317 -20
- package/cli/handlers.js +9 -1
- package/cli/interactive.js +4 -3
- package/cli/tokenSupplyAudit.js +90 -0
- package/msb.mjs +1 -0
- package/package.json +15 -6
- package/rpc/create_server.js +12 -3
- package/rpc/handlers.js +103 -0
- package/rpc/routes/v1.js +8 -0
- package/src/codecs/apply/applyOperationCodec.js +334 -0
- package/src/codecs/apply/applyOperations.generated.cjs +6380 -0
- package/src/codecs/consensus/v1/consensusV1.generated.cjs +3508 -0
- package/src/codecs/consensus/v1/consensusV1OperationCodec.js +161 -0
- package/src/codecs/consensus/v1/vdfConfigCodec.js +83 -0
- package/src/{utils/protobuf → codecs/network/v1}/networkV1.generated.cjs +664 -393
- package/src/codecs/network/v1/networkV1OperationCodec.js +25 -0
- package/src/codecs/protobufRuntime.cjs +95 -0
- package/src/codecs/state/escrowEntryCodec.js +45 -0
- package/src/codecs/state/state.generated.cjs +659 -0
- package/src/config/args.js +0 -1
- package/src/config/config.js +46 -7
- package/src/config/env.js +33 -6
- package/src/core/consensus/epochAppenderSelection/EpochAppenderSelectionError.js +55 -0
- package/src/core/consensus/epochAppenderSelection/EpochAppenderSelectionOperations.js +118 -0
- package/src/core/consensus/epochAppenderSelection/EpochAppenderSelector.js +1042 -0
- package/src/core/consensus/epochAppenderSelection/createEpochAppenderSelector.js +23 -0
- package/src/core/consensus/epochAppenderSelection/epochAppenderSelectionLedgerReader.js +274 -0
- package/src/core/consensus/epochAppenderSelection/epochAppenderSelectionState.js +488 -0
- package/src/core/consensus/epochAppenderSelection/epochAppenderSelectionStorage.js +93 -0
- package/src/core/consensus/quorum.js +11 -0
- package/src/core/consensus/services/EpochCoordinationRound.js +839 -0
- package/src/core/consensus/services/EpochCoordinatorOperations.js +211 -0
- package/src/core/consensus/services/EpochCoordinatorService.js +335 -0
- package/src/core/consensus/services/EpochRoundListeners.js +116 -0
- package/src/core/consensus/services/EpochStateMachine.js +141 -0
- package/src/core/consensus/services/VDFBare.js +291 -0
- package/src/core/consensus/services/VDFNode.js +111 -0
- package/src/core/consensus/services/VDFProgressWatchdog.js +79 -0
- package/src/core/consensus/services/VDFService.js +87 -0
- package/src/core/consensus/services/VDFServiceManager.js +147 -0
- package/src/core/consensus/services/createVDFService.js +16 -0
- package/src/core/consensus/services/vdf-guardian.js +32 -0
- package/src/core/consensus/services/vdf-worker.js +90 -0
- package/src/core/network/Network.js +327 -238
- package/src/core/network/protocols/consensus/IndexerMessages.js +72 -0
- package/src/core/network/protocols/consensus/v1/ConsensusV1ConnectionPolicy.js +57 -0
- package/src/core/network/protocols/consensus/v1/ConsensusV1MessageRouter.js +147 -0
- package/src/core/network/protocols/consensus/v1/ConsensusV1Protocol.js +72 -0
- package/src/core/network/protocols/consensus/v1/IndexerPendingRequestService.js +108 -0
- package/src/core/network/protocols/consensus/v1/V1ConsensusProtocolError.js +29 -0
- package/src/core/network/protocols/consensus/v1/handlers/V1EpochAppendOperationHandler.js +68 -0
- package/src/core/network/protocols/consensus/v1/handlers/V1EpochProofProposalOperationHandler.js +226 -0
- package/src/core/network/protocols/consensus/v1/validators/ConsensusValidationSchema.js +192 -0
- package/src/core/network/protocols/consensus/v1/validators/V1BaseConsensusOperation.js +428 -0
- package/src/core/network/protocols/consensus/v1/validators/V1EpochAppendReply.js +47 -0
- package/src/core/network/protocols/consensus/v1/validators/V1EpochAppendRequest.js +17 -0
- package/src/core/network/protocols/consensus/v1/validators/V1EpochProofProposalApproval.js +116 -0
- package/src/core/network/protocols/consensus/v1/validators/V1EpochProofProposalRequest.js +152 -0
- package/src/core/network/protocols/consensus/v1/validators/consensusValidationSchemaHelpers.js +145 -0
- package/src/core/network/protocols/shared/ConnectionOperationHandler.js +27 -0
- package/src/core/network/protocols/{NetworkMessages.js → validators/ValidatorMessages.js} +24 -14
- package/src/core/network/protocols/{ProtocolSession.js → validators/ValidatorProtocolSession.js} +22 -25
- package/src/core/network/protocols/{legacy/NetworkMessageRouter.js → validators/legacy/ValidatorLegacyMessageRouter.js} +6 -6
- package/src/core/network/protocols/{LegacyProtocol.js → validators/legacy/ValidatorLegacyProtocol.js} +9 -7
- package/src/core/network/protocols/{legacy → validators/legacy}/handlers/BaseStateOperationHandler.js +1 -1
- package/src/core/network/protocols/{legacy → validators/legacy}/handlers/LegacyGetRequestHandler.js +5 -5
- package/src/core/network/protocols/{legacy → validators/legacy}/handlers/LegacyRoleOperationHandler.js +5 -5
- package/src/core/network/protocols/{legacy → validators/legacy}/handlers/LegacySubnetworkOperationHandler.js +5 -5
- package/src/core/network/protocols/{legacy → validators/legacy}/handlers/LegacyTransferOperationHandler.js +5 -5
- package/src/core/network/protocols/{ProtocolInterface.js → validators/shared/ValidatorProtocolInterface.js} +5 -5
- package/src/core/network/protocols/{shared → validators/shared}/validators/PartialBootstrapDeploymentValidator.js +1 -1
- package/src/core/network/protocols/validators/shared/validators/PartialHtlcValidator.js +325 -0
- package/src/core/network/protocols/{shared → validators/shared}/validators/PartialOperationValidator.js +44 -21
- package/src/core/network/protocols/{shared → validators/shared}/validators/PartialRoleAccessValidator.js +3 -3
- package/src/core/network/protocols/{shared → validators/shared}/validators/PartialTransactionValidator.js +3 -3
- package/src/core/network/protocols/{shared → validators/shared}/validators/PartialTransferValidator.js +4 -4
- package/src/core/network/protocols/{v1 → validators/v1}/V1ProtocolError.js +1 -1
- package/src/core/network/{services/PendingRequestService.js → protocols/validators/v1/ValidatorPendingRequestService.js} +19 -82
- package/src/core/network/protocols/{v1/NetworkMessageRouter.js → validators/v1/ValidatorV1MessageRouter.js} +9 -9
- package/src/core/network/protocols/{V1Protocol.js → validators/v1/ValidatorV1Protocol.js} +10 -8
- package/src/core/network/protocols/{connectionPolicies.js → validators/v1/connectionPolicies.js} +1 -1
- package/src/core/network/protocols/{v1 → validators/v1}/handlers/V1BaseOperationHandler.js +6 -23
- package/src/core/network/protocols/{v1 → validators/v1}/handlers/V1BroadcastTransactionOperationHandler.js +14 -9
- package/src/core/network/protocols/{v1 → validators/v1}/handlers/V1LivenessOperationHandler.js +5 -4
- package/src/core/network/protocols/{v1 → validators/v1}/handlers/broadcastTransaction/BroadcastTransactionErrorMapper.js +3 -3
- package/src/core/network/protocols/{v1 → validators/v1}/handlers/broadcastTransaction/BroadcastTransactionOperationStrategies.js +44 -1
- package/src/core/network/protocols/{v1 → validators/v1}/handlers/broadcastTransaction/BroadcastTransactionPayloadUtils.js +1 -1
- package/src/core/network/protocols/{v1 → validators/v1}/validators/V1BaseOperation.js +19 -15
- package/src/core/network/protocols/{v1 → validators/v1}/validators/V1BroadcastTransactionRequest.js +2 -2
- package/src/core/network/protocols/{v1 → validators/v1}/validators/V1BroadcastTransactionResponse.js +34 -18
- package/src/core/network/protocols/{v1 → validators/v1}/validators/V1ValidationSchema.js +3 -3
- package/src/core/network/services/IndexerConnectionManager.js +88 -0
- package/src/core/network/services/TransactionPoolService.js +11 -18
- package/src/core/network/services/TransactionRateLimiterService.js +1 -1
- package/src/core/network/services/ValidatorConnectionManager.js +163 -0
- package/src/core/network/services/{MessageOrchestrator.js → ValidatorMessageOrchestrator.js} +34 -42
- package/src/core/network/services/ValidatorObserverService.js +21 -36
- package/src/core/network/shared/BasePendingRequestService.js +67 -0
- package/src/core/network/shared/PeerConnectionManager.js +185 -0
- package/src/core/state/ConfirmationWatch.js +82 -0
- package/src/core/state/State.js +1794 -173
- package/src/core/state/StateEventQueue.js +69 -0
- package/src/core/state/utils/address.js +10 -6
- package/src/core/state/utils/epochProof.js +109 -0
- package/src/core/state/utils/escrowEntry.js +178 -0
- package/src/core/state/validators/StateValidationSchema.js +757 -0
- package/src/core/state/validators/stateValidationRules.js +312 -0
- package/src/index.js +255 -65
- package/src/messages/consensus/v1/ConsensusMessageBuilder.js +368 -0
- package/src/messages/consensus/v1/ConsensusMessageDirector.js +142 -0
- package/src/messages/consensus/v1/consensusMessageFactory.js +12 -0
- package/src/messages/consensus/v1/epochAppendMessage.js +58 -0
- package/src/messages/network/v1/NetworkMessageBuilder.js +15 -7
- package/src/messages/state/ApplyStateMessageBuilder.js +499 -13
- package/src/messages/state/ApplyStateMessageDirector.js +261 -2
- package/src/utils/StateMachine.js +125 -0
- package/src/utils/applyOperations.js +45 -2
- package/src/utils/buffer.js +239 -59
- package/src/utils/consensusConfig.js +76 -0
- package/src/utils/constants.js +174 -73
- package/src/utils/helpers.js +0 -25
- package/src/utils/htlcLock.js +122 -0
- package/src/utils/logger.js +10 -4
- package/src/utils/migrationUtils.js +2 -1
- package/src/utils/normalizers.js +113 -0
- package/src/utils/scheduler/SchedulableService.js +107 -0
- package/src/utils/{Scheduler.js → scheduler/Scheduler.js} +56 -17
- package/src/core/network/services/ConnectionManager.js +0 -406
- package/src/utils/check.js +0 -525
- package/src/utils/protobuf/applyOperations.cjs +0 -1373
- package/src/utils/protobuf/operationHelpers.js +0 -81
- /package/src/core/network/protocols/{legacy → validators/legacy}/handlers/LegacyResponseHandler.js +0 -0
- /package/src/core/network/protocols/{legacy → validators/legacy}/validators/ValidatorResponse.js +0 -0
- /package/src/core/network/protocols/{legacy → validators/legacy}/validators/base/BaseResponse.js +0 -0
- /package/src/core/network/protocols/{v1 → validators/v1}/validators/V1LivenessRequest.js +0 -0
- /package/src/core/network/protocols/{v1 → validators/v1}/validators/V1LivenessResponse.js +0 -0
package/src/core/network/services/{MessageOrchestrator.js → ValidatorMessageOrchestrator.js}
RENAMED
|
@@ -4,15 +4,15 @@ import { networkMessageFactory } from "../../../messages/network/v1/networkMessa
|
|
|
4
4
|
import { NETWORK_CAPABILITIES, ResultCode } from "../../../utils/constants.js";
|
|
5
5
|
import {
|
|
6
6
|
unsafeEncodeApplyOperation
|
|
7
|
-
} from "../../../
|
|
7
|
+
} from "../../../codecs/apply/applyOperationCodec.js";
|
|
8
8
|
import { normalizeMessageByOperationType } from "../../../utils/normalizers.js";
|
|
9
|
-
import { resultToValidatorAction, SENDER_ACTION } from "../protocols/connectionPolicies.js";
|
|
10
|
-
import {
|
|
9
|
+
import { resultToValidatorAction, SENDER_ACTION } from "../protocols/validators/v1/connectionPolicies.js";
|
|
10
|
+
import { isPeerConnectionError } from '../shared/PeerConnectionManager.js';
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
* It works with
|
|
12
|
+
* ValidatorMessageOrchestrator coordinates message submission, retry, and validator management.
|
|
13
|
+
* It works with ValidatorConnectionManager and ledger state to ensure reliable message delivery.
|
|
14
14
|
*/
|
|
15
|
-
class
|
|
15
|
+
class ValidatorMessageOrchestrator {
|
|
16
16
|
#config;
|
|
17
17
|
#wallet;
|
|
18
18
|
#idempotentSuccessCodes = new Set([
|
|
@@ -21,18 +21,14 @@ class MessageOrchestrator {
|
|
|
21
21
|
]);
|
|
22
22
|
/**
|
|
23
23
|
* Attempts to send a message to validators with retries and state checks.
|
|
24
|
-
* @param {
|
|
24
|
+
* @param {ValidatorConnectionManager} validatorConnectionManager - The connection manager instance
|
|
25
25
|
* @param {object} state - The state to look for the message outcome
|
|
26
26
|
* @param {Config} config - Configuration options:
|
|
27
27
|
*/
|
|
28
|
-
constructor(
|
|
29
|
-
this.
|
|
28
|
+
constructor(validatorConnectionManager, state, config, wallet) {
|
|
29
|
+
this.validatorConnectionManager = validatorConnectionManager;
|
|
30
30
|
this.state = state;
|
|
31
31
|
this.#config = config;
|
|
32
|
-
this.#wallet = null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
setWallet(wallet) {
|
|
36
32
|
this.#wallet = wallet;
|
|
37
33
|
}
|
|
38
34
|
|
|
@@ -50,11 +46,10 @@ class MessageOrchestrator {
|
|
|
50
46
|
*/
|
|
51
47
|
#pickValidatorForMessage(message) {
|
|
52
48
|
const requesterAddress = message?.address;
|
|
53
|
-
if (!requesterAddress || typeof this.connectionManager.connectedValidators !== 'function') {
|
|
54
|
-
return this.connectionManager.pickRandomConnectedValidator();
|
|
55
|
-
}
|
|
56
49
|
|
|
57
|
-
|
|
50
|
+
if (!requesterAddress) return null
|
|
51
|
+
|
|
52
|
+
const connected = this.validatorConnectionManager.connectedPeers();
|
|
58
53
|
if (!Array.isArray(connected) || connected.length === 0) {
|
|
59
54
|
return null;
|
|
60
55
|
}
|
|
@@ -64,9 +59,6 @@ class MessageOrchestrator {
|
|
|
64
59
|
});
|
|
65
60
|
|
|
66
61
|
const pool = eligible.length > 0 ? eligible : connected;
|
|
67
|
-
if (typeof this.connectionManager.pickRandomValidator === 'function') {
|
|
68
|
-
return this.connectionManager.pickRandomValidator(pool);
|
|
69
|
-
}
|
|
70
62
|
|
|
71
63
|
const index = Math.floor(Math.random() * pool.length);
|
|
72
64
|
return pool[index] ?? null;
|
|
@@ -80,7 +72,7 @@ class MessageOrchestrator {
|
|
|
80
72
|
*/
|
|
81
73
|
async send(message, retries = 0) {
|
|
82
74
|
if (retries > this.#config.maxRetries) {
|
|
83
|
-
console.warn(`
|
|
75
|
+
console.warn(`ValidatorMessageOrchestrator: Max retries reached for message ${JSON.stringify(message)}. Aborting send.`);
|
|
84
76
|
return false;
|
|
85
77
|
}
|
|
86
78
|
|
|
@@ -89,19 +81,19 @@ class MessageOrchestrator {
|
|
|
89
81
|
console.log("Sending message to validator:", publicKeyToAddress(validatorPublicKey, this.#config));
|
|
90
82
|
|
|
91
83
|
/* NOTE: Since the retry logic for Legacy is handled here, and is very unique to the protocol,
|
|
92
|
-
* it was decided to not change
|
|
84
|
+
* it was decided to not change ValidatorMessageOrchestrator send method in the refactor to make protocols transparent.
|
|
93
85
|
* As the Legacy protocol is going to be deprecated soon, it was decided to keep the retry logic
|
|
94
86
|
* here instead of abstracting it in the protocol implementation.
|
|
95
|
-
* If we were to abstract it, we would need to add protocol-specific logic in the
|
|
96
|
-
* or
|
|
87
|
+
* If we were to abstract it, we would need to add protocol-specific logic in the ValidatorProtocolSession
|
|
88
|
+
* or ValidatorProtocolInterface, which would make them less clean and more coupled with the specifics of the protocols.
|
|
97
89
|
* The parts to be refactored in the future are marked with TODO comments.
|
|
98
90
|
*/
|
|
99
91
|
|
|
100
92
|
// TODO: After Legacy is deprecated, we don't need to check preferred protocol here.
|
|
101
|
-
const validatorConnection = this.
|
|
102
|
-
const preferredProtocol = validatorConnection.
|
|
93
|
+
const validatorConnection = this.validatorConnectionManager.getConnection(validatorPublicKey);
|
|
94
|
+
const preferredProtocol = validatorConnection.protocolSessions.validator.preferredProtocol;
|
|
103
95
|
let success = false;
|
|
104
|
-
if (preferredProtocol === validatorConnection.
|
|
96
|
+
if (preferredProtocol === validatorConnection.protocolSessions.validator.supportedProtocols.LEGACY) {
|
|
105
97
|
|
|
106
98
|
try {
|
|
107
99
|
success = await this.#attemptSendMessageForLegacy(validatorPublicKey, message);
|
|
@@ -110,10 +102,10 @@ class MessageOrchestrator {
|
|
|
110
102
|
}
|
|
111
103
|
if (!success) {
|
|
112
104
|
// Remove validator and retry
|
|
113
|
-
this.
|
|
105
|
+
this.validatorConnectionManager.remove(validatorPublicKey);
|
|
114
106
|
success = await this.send(message, retries + 1);
|
|
115
107
|
}
|
|
116
|
-
} else if (preferredProtocol === validatorConnection.
|
|
108
|
+
} else if (preferredProtocol === validatorConnection.protocolSessions.validator.supportedProtocols.V1) {
|
|
117
109
|
// TODO: This is probably better placed inside the V1 protocol definition.
|
|
118
110
|
// Both protocols should receive a 'canonical' message and solve the encodings internally
|
|
119
111
|
// Refactor
|
|
@@ -126,7 +118,7 @@ class MessageOrchestrator {
|
|
|
126
118
|
NETWORK_CAPABILITIES
|
|
127
119
|
);
|
|
128
120
|
|
|
129
|
-
await this.
|
|
121
|
+
await this.validatorConnectionManager.sendSingleMessage(v1Message, validatorPublicKey)
|
|
130
122
|
.then(
|
|
131
123
|
async (resultCode) => {
|
|
132
124
|
if (await this.#isIdempotentSuccess(resultCode, message)) {
|
|
@@ -142,19 +134,19 @@ class MessageOrchestrator {
|
|
|
142
134
|
//TODO: Create a function for action below, and replace it also in legacy flow.
|
|
143
135
|
this.incrementSentCount(validatorPublicKey);
|
|
144
136
|
if (this.shouldRemove(validatorPublicKey)) {
|
|
145
|
-
this.
|
|
137
|
+
this.validatorConnectionManager.remove(validatorPublicKey);
|
|
146
138
|
}
|
|
147
139
|
break;
|
|
148
140
|
case SENDER_ACTION.ROTATE:
|
|
149
|
-
this.
|
|
141
|
+
this.validatorConnectionManager.remove(validatorPublicKey);
|
|
150
142
|
break;
|
|
151
143
|
case SENDER_ACTION.NO_ROTATE:
|
|
152
144
|
// ignore
|
|
153
145
|
break;
|
|
154
146
|
default:
|
|
155
|
-
this.
|
|
147
|
+
this.validatorConnectionManager.remove(validatorPublicKey);
|
|
156
148
|
console.warn(
|
|
157
|
-
`
|
|
149
|
+
`ValidatorMessageOrchestrator: Unrecognized action from connectionPolicies: ${action}.
|
|
158
150
|
ResultCode was: ${resultCode}. Removing validator ${publicKeyToAddress(validatorPublicKey, this.#config)}`
|
|
159
151
|
);
|
|
160
152
|
break;
|
|
@@ -163,11 +155,11 @@ class MessageOrchestrator {
|
|
|
163
155
|
)
|
|
164
156
|
.catch(
|
|
165
157
|
async (err) => {
|
|
166
|
-
if (err
|
|
158
|
+
if (isPeerConnectionError(err)) {
|
|
167
159
|
success = await this.send(message, retries + 1);
|
|
168
|
-
console.warn(`
|
|
160
|
+
console.warn(`ValidatorMessageOrchestrator: Connection Error: ${err.message}`);
|
|
169
161
|
} else {
|
|
170
|
-
this.
|
|
162
|
+
this.validatorConnectionManager.remove(validatorPublicKey);
|
|
171
163
|
success = await this.send(message, retries + 1);
|
|
172
164
|
}
|
|
173
165
|
}
|
|
@@ -216,7 +208,7 @@ class MessageOrchestrator {
|
|
|
216
208
|
// TODO: Delete this function after legacy protocol is deprecated
|
|
217
209
|
async #attemptSendMessageForLegacy(validatorPublicKey, message) {
|
|
218
210
|
const deductedTxType = operationToPayload(message.type);
|
|
219
|
-
await this.
|
|
211
|
+
await this.validatorConnectionManager.sendSingleMessage(message, validatorPublicKey);
|
|
220
212
|
const appeared = await this.state.waitForUnsigned(
|
|
221
213
|
message[deductedTxType].tx,
|
|
222
214
|
this.#config.messageValidatorResponseTimeout
|
|
@@ -224,7 +216,7 @@ class MessageOrchestrator {
|
|
|
224
216
|
if (appeared) {
|
|
225
217
|
this.incrementSentCount(validatorPublicKey);
|
|
226
218
|
if (this.shouldRemove(validatorPublicKey)) {
|
|
227
|
-
this.
|
|
219
|
+
this.validatorConnectionManager.remove(validatorPublicKey);
|
|
228
220
|
}
|
|
229
221
|
return true;
|
|
230
222
|
}
|
|
@@ -232,12 +224,12 @@ class MessageOrchestrator {
|
|
|
232
224
|
}
|
|
233
225
|
|
|
234
226
|
incrementSentCount(validatorPubKey) {
|
|
235
|
-
this.
|
|
227
|
+
this.validatorConnectionManager.incrementSentCount(validatorPubKey);
|
|
236
228
|
}
|
|
237
229
|
|
|
238
230
|
shouldRemove(validatorPubKey) {
|
|
239
|
-
return this.
|
|
231
|
+
return this.validatorConnectionManager.getSentCount(validatorPubKey) >= this.#config.messageThreshold;
|
|
240
232
|
}
|
|
241
233
|
}
|
|
242
234
|
|
|
243
|
-
export default
|
|
235
|
+
export default ValidatorMessageOrchestrator;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import SchedulableService from "../../../utils/scheduler/SchedulableService.js";
|
|
2
2
|
import { Logger } from "../../../utils/logger.js";
|
|
3
3
|
import b4a from "b4a";
|
|
4
4
|
import { bufferToAddress } from "../../../core/state/utils/address.js";
|
|
@@ -9,18 +9,16 @@ import { WRITER_BYTE_LENGTH, CONNECTION_STATUS } from "../../../utils/constants.
|
|
|
9
9
|
const VALIDATOR_CANDIDATES_PER_CYCLE = 10;
|
|
10
10
|
const MAX_KEY_DECODE_CACHE_SIZE = 10_000;
|
|
11
11
|
|
|
12
|
-
class ValidatorObserverService {
|
|
12
|
+
class ValidatorObserverService extends SchedulableService {
|
|
13
13
|
#network;
|
|
14
14
|
#state;
|
|
15
15
|
#address;
|
|
16
16
|
#config;
|
|
17
|
-
#scheduler;
|
|
18
17
|
#logger;
|
|
19
18
|
#hasBootstrapped; // Whether initial connection phase is complete
|
|
20
19
|
#bootstrapStartedAt; // Timestamp to enforce bootstrap timeout
|
|
21
20
|
#adminCache; // Cached admin entry with TTL
|
|
22
21
|
#writersCache; // Cached writer list with dynamic TTL
|
|
23
|
-
#isInterrupted; // Used to stop execution mid-cycle
|
|
24
22
|
#keyDecodeCache;
|
|
25
23
|
|
|
26
24
|
/**
|
|
@@ -30,6 +28,7 @@ class ValidatorObserverService {
|
|
|
30
28
|
* @param {Object} config - The application configuration object.
|
|
31
29
|
*/
|
|
32
30
|
constructor(network, state, address, config) {
|
|
31
|
+
super();
|
|
33
32
|
this.#network = network;
|
|
34
33
|
this.#state = state;
|
|
35
34
|
this.#address = address;
|
|
@@ -39,18 +38,9 @@ class ValidatorObserverService {
|
|
|
39
38
|
this.#bootstrapStartedAt = 0;
|
|
40
39
|
this.#adminCache = { entry: null, lastUpdated: 0 };
|
|
41
40
|
this.#writersCache = { list: [], lastUpdated: 0 };
|
|
42
|
-
this.#isInterrupted = false;
|
|
43
41
|
this.#keyDecodeCache = new Map();
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
/**
|
|
47
|
-
* Returns configurable poll interval.
|
|
48
|
-
* @returns {number} The poll interval in milliseconds.
|
|
49
|
-
*/
|
|
50
|
-
#getPollInterval() {
|
|
51
|
-
return this.#config.pollInterval;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
44
|
/**
|
|
55
45
|
* Selects a subset of valid validator candidates to connect to.
|
|
56
46
|
*
|
|
@@ -79,7 +69,7 @@ class ValidatorObserverService {
|
|
|
79
69
|
const target = Math.min(networkSize, this.#config.maxValidators);
|
|
80
70
|
|
|
81
71
|
const isSelfAdmin = this.#address === adminEntry?.address;
|
|
82
|
-
const totalConnected = manager.
|
|
72
|
+
const totalConnected = manager.connectionCount();
|
|
83
73
|
|
|
84
74
|
const available = writers.filter((w) => {
|
|
85
75
|
if (w.address === this.#address) return false;
|
|
@@ -144,7 +134,7 @@ class ValidatorObserverService {
|
|
|
144
134
|
// Non-admin nodes: ensure the admin is not kept as a validator connection
|
|
145
135
|
const adminWriter = writers.find((w) => w.address === adminEntry?.address);
|
|
146
136
|
if (adminWriter?.publicKey) {
|
|
147
|
-
this.#network.validatorConnectionManager.remove(adminWriter.publicKey
|
|
137
|
+
this.#network.validatorConnectionManager.remove(adminWriter.publicKey);
|
|
148
138
|
}
|
|
149
139
|
}
|
|
150
140
|
|
|
@@ -157,7 +147,7 @@ class ValidatorObserverService {
|
|
|
157
147
|
|
|
158
148
|
if (manager.connected(publicKey)) {
|
|
159
149
|
// leave the transport to the swarm
|
|
160
|
-
manager.remove(publicKey
|
|
150
|
+
manager.remove(publicKey);
|
|
161
151
|
this.#logger.debug(`Removed stale validator connection: ${b4a.toString(publicKey, "hex")}`);
|
|
162
152
|
}
|
|
163
153
|
}
|
|
@@ -168,7 +158,7 @@ class ValidatorObserverService {
|
|
|
168
158
|
* @returns {boolean} True if the observer is enabled and not interrupted.
|
|
169
159
|
*/
|
|
170
160
|
#shouldRun() {
|
|
171
|
-
return this
|
|
161
|
+
return !this.isInterrupted;
|
|
172
162
|
}
|
|
173
163
|
|
|
174
164
|
/**
|
|
@@ -214,18 +204,10 @@ class ValidatorObserverService {
|
|
|
214
204
|
this.#logger.info('ValidatorObserverService can not start. Disabled by configuration.');
|
|
215
205
|
return;
|
|
216
206
|
}
|
|
217
|
-
|
|
218
|
-
if (this.#scheduler && this.#scheduler.isRunning) {
|
|
219
|
-
this.#logger.info('ValidatorObserverService is already started');
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
207
|
|
|
223
|
-
const interval = this.#getPollInterval();
|
|
224
|
-
this.#scheduler = new Scheduler((next) => this.#worker(next), interval);
|
|
225
208
|
this.#bootstrapStartedAt = Date.now();
|
|
226
209
|
this.#hasBootstrapped = false;
|
|
227
|
-
|
|
228
|
-
this.#scheduler.start();
|
|
210
|
+
super.start();
|
|
229
211
|
this.#logger.debug("ValidatorObserverService started");
|
|
230
212
|
}
|
|
231
213
|
|
|
@@ -234,13 +216,9 @@ class ValidatorObserverService {
|
|
|
234
216
|
* Prevents new work and waits for the current cycle to finish if requested.
|
|
235
217
|
* @param {boolean} [waitForCurrent=true] - Whether to wait for the current worker cycle to finish.
|
|
236
218
|
*/
|
|
237
|
-
async
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
this.#isInterrupted = true;
|
|
241
|
-
|
|
242
|
-
await this.#scheduler.stop(waitForCurrent);
|
|
243
|
-
this.#scheduler = null;
|
|
219
|
+
async stop(waitForCurrent = true) {
|
|
220
|
+
const stopped = await super.stop(waitForCurrent);
|
|
221
|
+
if (!stopped) return;
|
|
244
222
|
this.#logger.debug("ValidatorObserverService stopped");
|
|
245
223
|
}
|
|
246
224
|
|
|
@@ -255,9 +233,16 @@ class ValidatorObserverService {
|
|
|
255
233
|
* 5. Attempt connections (with pacing).
|
|
256
234
|
* @param {Function} next - Callback to schedule the next execution.
|
|
257
235
|
*/
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
236
|
+
getScheduleInterval() {
|
|
237
|
+
return this.#config.pollInterval;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async worker(next) {
|
|
241
|
+
const interval = this.#config.pollInterval;
|
|
242
|
+
if (!this.#shouldRun()) {
|
|
243
|
+
next(interval);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
261
246
|
|
|
262
247
|
try {
|
|
263
248
|
const adminEntry = await this.#getAdminEntryCached();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export default class BasePendingRequestService {
|
|
2
|
+
_pendingRequests = new Map();
|
|
3
|
+
|
|
4
|
+
has(id) {
|
|
5
|
+
return this._pendingRequests.has(id);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
getAndDeletePendingRequest(id) {
|
|
9
|
+
const entry = this._pendingRequests.get(id);
|
|
10
|
+
if (!entry) return null;
|
|
11
|
+
|
|
12
|
+
clearTimeout(entry.timeoutId);
|
|
13
|
+
this._pendingRequests.delete(id);
|
|
14
|
+
return entry;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
getPendingRequest(id) {
|
|
18
|
+
const entry = this._pendingRequests.get(id);
|
|
19
|
+
if (!entry) return null;
|
|
20
|
+
return entry;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
rejectPendingRequest(id, error) {
|
|
24
|
+
const entry = this.getAndDeletePendingRequest(id);
|
|
25
|
+
if (!entry) return false;
|
|
26
|
+
entry.reject(error instanceof Error ? error : new Error(error?.message ?? 'Unexpected error'));
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
rejectPendingRequestsForPeer(peerPubKeyHex, error) {
|
|
31
|
+
const idsToReject = [];
|
|
32
|
+
for (const [id, entry] of this._pendingRequests) {
|
|
33
|
+
if (entry.requestedTo === peerPubKeyHex) idsToReject.push(id);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const id of idsToReject) {
|
|
37
|
+
this.rejectPendingRequest(id, error);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return idsToReject.length;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
stopPendingRequestTimeout(id) {
|
|
44
|
+
const entry = this._pendingRequests.get(id);
|
|
45
|
+
if (!entry) return false;
|
|
46
|
+
|
|
47
|
+
clearTimeout(entry.timeoutId);
|
|
48
|
+
entry.timeoutId = null;
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
close() {
|
|
53
|
+
for (const [id, entry] of this._pendingRequests) {
|
|
54
|
+
clearTimeout(entry.timeoutId);
|
|
55
|
+
try {
|
|
56
|
+
entry.reject(this._createShutdownError(id));
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(`${this.constructor.name}.close: failed to reject pending request ${id}:`, error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
this._pendingRequests.clear();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_createShutdownError(id) {
|
|
65
|
+
return new Error(`Pending request ${id} cancelled (shutdown).`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import ReadyResource from "ready-resource"
|
|
2
|
+
import { toHex } from "../../../utils/buffer.js";
|
|
3
|
+
import { publicKeyToAddress } from "../../../utils/helpers.js";
|
|
4
|
+
|
|
5
|
+
export class PeerConnectionManagerError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = this.constructor.name;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class PeerConnectionManager extends ReadyResource {
|
|
13
|
+
_connections = new Map();
|
|
14
|
+
_max;
|
|
15
|
+
|
|
16
|
+
constructor(max, config, logger) {
|
|
17
|
+
super();
|
|
18
|
+
this._max = max;
|
|
19
|
+
this._config = config;
|
|
20
|
+
this._logger = logger
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
setMax(maxPeers) {
|
|
25
|
+
this._max = maxPeers;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_toHexString(publicKey) {
|
|
29
|
+
return toHex(publicKey);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Adds a peer to the pool if not already present.
|
|
34
|
+
* @param {String | Buffer} publicKey - The public key hex string of the peer to add
|
|
35
|
+
* @param {Object} connection - The connection object associated with the peer
|
|
36
|
+
* @returns {Boolean} - Returns true if the peer was added or updated, false otherwise
|
|
37
|
+
*/
|
|
38
|
+
_add(publicKey, connection) {
|
|
39
|
+
const publicKeyHex = this._toHexString(publicKey);
|
|
40
|
+
if (this.maxConnectionsReached()) {
|
|
41
|
+
this._logger.debug('add: max connections reached.');
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
this._logger.debug(`add: adding peer ${publicKeyToAddress(publicKeyHex, this._config)}`);
|
|
45
|
+
if (!this.exists(publicKeyHex)) {
|
|
46
|
+
this._logger.debug(`add: appending peer ${publicKeyToAddress(publicKeyHex, this._config)}`);
|
|
47
|
+
this.#append(publicKeyHex, connection);
|
|
48
|
+
return true;
|
|
49
|
+
} else if (!this.connected(publicKeyHex)) {
|
|
50
|
+
this._logger.debug(`add: updating peer ${publicKeyToAddress(publicKeyHex, this._config)}`);
|
|
51
|
+
this.#update(publicKeyHex, connection);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this._logger.debug(`add: didn't add peer ${publicKeyToAddress(publicKeyHex, this._config)}`);
|
|
56
|
+
return false; // TODO: Implement better success/failure reporting
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Checks if a peer exists in the pool.
|
|
61
|
+
* @param {String | Buffer} publicKey - The public key hex string of the peer to check
|
|
62
|
+
* @returns {Boolean} - Returns true if the peer exists, false otherwise
|
|
63
|
+
*/
|
|
64
|
+
exists(publicKey) {
|
|
65
|
+
const publicKeyHex = this._toHexString(publicKey);
|
|
66
|
+
return this._connections.has(publicKeyHex);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Checks if a peer is currently connected.
|
|
71
|
+
* @param {String | Buffer} publicKey - The public key hex string of the peer to check
|
|
72
|
+
* @returns {Boolean} - Returns true if the peer is connected, false otherwise
|
|
73
|
+
*/
|
|
74
|
+
connected(publicKey) {
|
|
75
|
+
const publicKeyHex = this._toHexString(publicKey);
|
|
76
|
+
return this.exists(publicKeyHex) && this._connections.get(publicKeyHex).connection !== null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Retrieves the connection object for a given peer public key.
|
|
81
|
+
* @param {String | Buffer} publicKey - The public key (Buffer or hex string) of the peer.
|
|
82
|
+
* @returns {Connection|undefined} - The connection object if found, otherwise undefined.
|
|
83
|
+
*/
|
|
84
|
+
getConnection(publicKey) {
|
|
85
|
+
const publicKeyHex = this._toHexString(publicKey);
|
|
86
|
+
const entry = this._connections.get(publicKeyHex);
|
|
87
|
+
return entry ? entry.connection : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Gets a list of all currently connected peers' public keys.
|
|
92
|
+
* @returns {Array} - An array of public key hex strings of connected peers
|
|
93
|
+
*/
|
|
94
|
+
connectedPeers() {
|
|
95
|
+
return Array.from(this._connections.keys()).filter(pk => this.connected(pk));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Gets the current number of connected peers.
|
|
100
|
+
* @returns {Number} - The count of connected peers
|
|
101
|
+
*/
|
|
102
|
+
connectionCount() {
|
|
103
|
+
return this.connectedPeers().length;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Checks if the maximum number of connections has been reached.
|
|
108
|
+
* @returns {Boolean} - Returns true if the maximum number of connections has been reached, false otherwise.
|
|
109
|
+
*/
|
|
110
|
+
// Note: this function name is a bit misleading. It checks if we have reached max connections and returns boolean
|
|
111
|
+
// The name leads to think it returns the number of max connections
|
|
112
|
+
maxConnectionsReached() {
|
|
113
|
+
return this.connectionCount() >= this._max;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
clear() {
|
|
117
|
+
this._connections.clear()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Appends a new peer connection.
|
|
122
|
+
* @param {String|Buffer} publicKey - The public key hex string of the peer
|
|
123
|
+
* @param {Object} connection - The connection object
|
|
124
|
+
*/
|
|
125
|
+
#append(publicKey, connection) {
|
|
126
|
+
this._logger.debug(`#append: appending peer ${publicKeyToAddress(publicKey, this._config)}`);
|
|
127
|
+
const publicKeyHex = this._toHexString(publicKey);
|
|
128
|
+
if (this._connections.has(publicKeyHex)) {
|
|
129
|
+
this._logger.debug(`#append: tried to append existing peer: ${publicKeyToAddress(publicKey, this._config)}`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this._connections.set(publicKeyHex, {connection, sent: 0});
|
|
133
|
+
connection.on('close', () => {
|
|
134
|
+
this._logger.debug(`#append: connection closing for peer ${publicKeyToAddress(publicKey, this._config)}`);
|
|
135
|
+
this.remove(publicKeyHex, connection);
|
|
136
|
+
this._logger.debug(`#append: connection closed for peer ${publicKeyToAddress(publicKey, this._config)}`);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Updates an existing peer connection or adds it if not present.
|
|
142
|
+
* @param {String|Buffer} publicKey - The public key hex string of the peer
|
|
143
|
+
* @param {Object} connection - The connection object
|
|
144
|
+
*/
|
|
145
|
+
#update(publicKey, connection) {
|
|
146
|
+
// Note: Is there a good reason for the function 'update' to exist separately from 'append'?
|
|
147
|
+
// It seems that both could be merged into a single function that either adds or updates the entry.
|
|
148
|
+
// It would be preferable to keep them separated though, but we would need to review all usages to ensure correctness.
|
|
149
|
+
// Also, we should remove the 'else' branch below if we decide to keep 'update' and 'append' separated.
|
|
150
|
+
const publicKeyHex = this._toHexString(publicKey);
|
|
151
|
+
this._logger.debug(`#update: updating peer ${publicKeyToAddress(publicKey, this._config)}`);
|
|
152
|
+
if (this._connections.has(publicKeyHex)) {
|
|
153
|
+
this._connections.get(publicKeyHex).connection = connection;
|
|
154
|
+
} else {
|
|
155
|
+
this._connections.set(publicKeyHex, {connection, sent: 0});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Gets the number of messages sent through a peer.
|
|
161
|
+
* @param {String | Buffer} publicKey - The public key hex string of the peer
|
|
162
|
+
* @returns {Number} - The count of messages sent
|
|
163
|
+
*/
|
|
164
|
+
getSentCount(publicKey) {
|
|
165
|
+
const publicKeyHex = this._toHexString(publicKey);
|
|
166
|
+
const entry = this._connections.get(publicKeyHex);
|
|
167
|
+
return entry ? (entry.sent || 0) : 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Increments the count of messages sent through a peer.
|
|
172
|
+
* @param {String | Buffer} publicKey - The public key hex string of the peer
|
|
173
|
+
*/
|
|
174
|
+
incrementSentCount(publicKey) {
|
|
175
|
+
const publicKeyHex = this._toHexString(publicKey);
|
|
176
|
+
const entry = this._connections.get(publicKeyHex);
|
|
177
|
+
if (entry) {
|
|
178
|
+
entry.sent = (entry.sent || 0) + 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function isPeerConnectionError(err) {
|
|
184
|
+
return (err instanceof PeerConnectionManagerError)
|
|
185
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const DEFAULT_CHECK_INTERVAL = 15_000;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Autobase starts a follower's fast-forward only when the network's signed length is at least
|
|
5
|
+
* 16 entries past the follower's total length, unconfirmed entries included. After a writer is
|
|
6
|
+
* removed in the middle of a batch, a follower that already holds unconfirmed entries can stay
|
|
7
|
+
* above that mark: its confirmed length then stops for good, and a restart does not help.
|
|
8
|
+
*
|
|
9
|
+
* This watch forces one fast-forward when the confirmed length has not moved for `timeout` ms
|
|
10
|
+
* while unconfirmed entries wait. It never runs on an indexer and does not change how entries are
|
|
11
|
+
* applied.
|
|
12
|
+
*/
|
|
13
|
+
export class ConfirmationWatch {
|
|
14
|
+
#base;
|
|
15
|
+
#timeout;
|
|
16
|
+
#interval;
|
|
17
|
+
#now;
|
|
18
|
+
#log;
|
|
19
|
+
#timer = null;
|
|
20
|
+
#mark = null;
|
|
21
|
+
#running = false;
|
|
22
|
+
|
|
23
|
+
constructor(base, { timeout, interval = DEFAULT_CHECK_INTERVAL, now = Date.now, log = console.log } = {}) {
|
|
24
|
+
this.#base = base;
|
|
25
|
+
this.#timeout = timeout;
|
|
26
|
+
this.#interval = Math.min(interval, timeout);
|
|
27
|
+
this.#now = now;
|
|
28
|
+
this.#log = log;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get enabled() {
|
|
32
|
+
return Number.isFinite(this.#timeout) && this.#timeout > 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
start() {
|
|
36
|
+
if (!this.enabled || this.#timer !== null) return;
|
|
37
|
+
this.#timer = setInterval(() => { this.check().catch(() => {}) }, this.#interval);
|
|
38
|
+
this.#timer.unref?.();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
stop() {
|
|
42
|
+
if (this.#timer !== null) clearInterval(this.#timer);
|
|
43
|
+
this.#timer = null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @returns {Promise<boolean>} true when a fast-forward was forced */
|
|
47
|
+
async check() {
|
|
48
|
+
const base = this.#base;
|
|
49
|
+
if (this.#running || base.closing || base.isIndexer) return false;
|
|
50
|
+
if (base.isFastForwarding?.()) {
|
|
51
|
+
this.#mark = null;
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const confirmed = base.view.core.signedLength;
|
|
56
|
+
const total = base.view.core.length;
|
|
57
|
+
const now = this.#now();
|
|
58
|
+
|
|
59
|
+
if (total <= confirmed) {
|
|
60
|
+
this.#mark = null;
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (this.#mark === null || this.#mark.confirmed !== confirmed) {
|
|
64
|
+
this.#mark = { confirmed, since: now };
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (now - this.#mark.since < this.#timeout) return false;
|
|
68
|
+
|
|
69
|
+
this.#running = true;
|
|
70
|
+
try {
|
|
71
|
+
this.#log(`[MSB] Confirmed length stuck at ${confirmed} (local ${total}) for ${Math.round((now - this.#mark.since) / 1000)} s; forcing a fast-forward`);
|
|
72
|
+
await base.forceFastForward();
|
|
73
|
+
this.#log(`[MSB] Fast-forward finished: confirmed ${base.view.core.signedLength}, local ${base.view.core.length}`);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
this.#log(`[MSB] Fast-forward failed: ${error?.message ?? error}`);
|
|
76
|
+
} finally {
|
|
77
|
+
this.#running = false;
|
|
78
|
+
this.#mark = { confirmed: base.view.core.signedLength, since: this.#now() };
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|