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.
- 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 +41 -7
- package/src/config/env.js +33 -9
- 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/State.js +1789 -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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stores the data used by one epoch round. Each approval attempt gets a new collection. Late callbacks
|
|
3
|
+
* must check that their collection is still current and open.
|
|
4
|
+
*
|
|
5
|
+
* @typedef {object} EpochRoundContext
|
|
6
|
+
* @property {function(number): Promise<boolean>} finishAndScheduleNextRound Closes this round and schedules the next one.
|
|
7
|
+
* @property {bigint} [currentEpoch] Signed parent epoch used to build the proof.
|
|
8
|
+
* @property {Buffer} [currentEpochHash] Hash of that parent record.
|
|
9
|
+
* @property {number} [vdfDifficulty] Signed VDF iteration count.
|
|
10
|
+
* @property {number} [vdfDiscriminantSize] Signed discriminant size in bits.
|
|
11
|
+
* @property {import('./EpochCoordinatorOperations.js').EpochVdfProof} [vdf] Local proof that can be reused while the parent epoch stays the same.
|
|
12
|
+
* @property {object} [proofProposalMessage] Signed message containing the local proof.
|
|
13
|
+
* @property {object} [proofProposal] Proof proposal fields from the signed message.
|
|
14
|
+
* @property {Set<string>} [indexerKeys] Writer keys captured for this attempt, encoded as hex.
|
|
15
|
+
* @property {number} [requiredProofSignatures] Proof quorum including the proposer's signature.
|
|
16
|
+
* @property {Array<{key: Buffer}>} [approvers] Other indexers that can sign this proof.
|
|
17
|
+
* @property {import('./EpochCoordinatorOperations.js').EpochApprovalCollection|null} [approvalCollection] This attempt's proof approvals.
|
|
18
|
+
* @property {Buffer} [setEpochPayload] Complete SET_EPOCH operation bytes.
|
|
19
|
+
* @property {boolean} [remoteProposalReceived] A validated proposal matches this epoch and parent.
|
|
20
|
+
* @property {boolean} [appendElectionRetry] Use the shorter election backoff for the next retry.
|
|
21
|
+
*/
|
|
22
|
+
import { StateMachine } from "../../../utils/StateMachine.js";
|
|
23
|
+
|
|
24
|
+
export const EPOCH_STATES = Object.freeze({
|
|
25
|
+
LOAD_EPOCH_CONTEXT: "LOAD_EPOCH_CONTEXT",
|
|
26
|
+
INITIALIZE_VDF: "INITIALIZE_VDF",
|
|
27
|
+
BUILD_AND_SIGN_PROOF_PROPOSAL: "BUILD_AND_SIGN_PROOF_PROPOSAL",
|
|
28
|
+
SEND_PROPOSAL_TO_INDEXERS: "SEND_PROPOSAL_TO_INDEXERS",
|
|
29
|
+
COLLECT_APPROVALS: "COLLECT_APPROVALS",
|
|
30
|
+
QUORUM_DECISION: "QUORUM_DECISION",
|
|
31
|
+
BUILD_SET_EPOCH: "BUILD_SET_EPOCH",
|
|
32
|
+
WAIT_FOR_APPEND_REPLIES: "WAIT_FOR_APPEND_REPLIES",
|
|
33
|
+
REFRESH_SIGNED_STATE_BEFORE_APPEND: "REFRESH_SIGNED_STATE_BEFORE_APPEND",
|
|
34
|
+
APPEND_SET_EPOCH: "APPEND_SET_EPOCH",
|
|
35
|
+
WAIT_FOR_APPEND_OUTCOME: "WAIT_FOR_APPEND_OUTCOME",
|
|
36
|
+
REFRESH_SIGNED_STATE: "REFRESH_SIGNED_STATE",
|
|
37
|
+
BACKOFF: "BACKOFF",
|
|
38
|
+
RELOAD_SIGNED_CONTEXT: "RELOAD_SIGNED_CONTEXT",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const EPOCH_EVENTS = Object.freeze({
|
|
42
|
+
START: "START",
|
|
43
|
+
LOCAL_PROOF_READY: "LOCAL_PROOF_READY",
|
|
44
|
+
PROPOSAL_BUILT: "PROPOSAL_BUILT",
|
|
45
|
+
EXTERNAL_APPROVALS_REQUIRED: "EXTERNAL_APPROVALS_REQUIRED",
|
|
46
|
+
EXTERNAL_APPROVALS_NOT_REQUIRED: "EXTERNAL_APPROVALS_NOT_REQUIRED",
|
|
47
|
+
PROPOSAL_BROADCAST: "PROPOSAL_BROADCAST",
|
|
48
|
+
QUORUM_REACHED: "QUORUM_REACHED",
|
|
49
|
+
APPROVAL_COLLECTION_FAILED: "APPROVAL_COLLECTION_FAILED",
|
|
50
|
+
SET_EPOCH_BUILT: "SET_EPOCH_BUILT",
|
|
51
|
+
APPEND_REPLIES_COLLECTED: "APPEND_REPLIES_COLLECTED",
|
|
52
|
+
APPEND_REPLY_COLLECTION_FAILED: "APPEND_REPLY_COLLECTION_FAILED",
|
|
53
|
+
APPEND_STARTED: "APPEND_STARTED",
|
|
54
|
+
APPEND_REJECTED: "APPEND_REJECTED",
|
|
55
|
+
APPENDER_SELECTION_EXPIRED: "APPENDER_SELECTION_EXPIRED",
|
|
56
|
+
TARGET_EPOCH_ABSENT: "TARGET_EPOCH_ABSENT",
|
|
57
|
+
TARGET_EPOCH_ALREADY_SIGNED: "TARGET_EPOCH_ALREADY_SIGNED",
|
|
58
|
+
TARGET_EPOCH_CONFIRMED: "TARGET_EPOCH_CONFIRMED",
|
|
59
|
+
BACKOFF_ELAPSED: "BACKOFF_ELAPSED",
|
|
60
|
+
EPOCH_UNCHANGED_WITHOUT_LOCAL_PROOF: "EPOCH_UNCHANGED_WITHOUT_LOCAL_PROOF",
|
|
61
|
+
EPOCH_UNCHANGED_WITH_LOCAL_PROOF: "EPOCH_UNCHANGED_WITH_LOCAL_PROOF",
|
|
62
|
+
NEW_EPOCH_DISCOVERED: "NEW_EPOCH_DISCOVERED",
|
|
63
|
+
SOLVER_FAILURE: "SOLVER FAILURE",
|
|
64
|
+
INDEXERS_CHANGED: "INDEXERS_CHANGED",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const TRANSITIONS = {
|
|
68
|
+
[EPOCH_STATES.LOAD_EPOCH_CONTEXT]: Object.freeze({
|
|
69
|
+
[EPOCH_EVENTS.START]: EPOCH_STATES.INITIALIZE_VDF,
|
|
70
|
+
}),
|
|
71
|
+
[EPOCH_STATES.INITIALIZE_VDF]: Object.freeze({
|
|
72
|
+
[EPOCH_EVENTS.LOCAL_PROOF_READY]: EPOCH_STATES.BUILD_AND_SIGN_PROOF_PROPOSAL,
|
|
73
|
+
[EPOCH_EVENTS.SOLVER_FAILURE]: EPOCH_STATES.BACKOFF,
|
|
74
|
+
}),
|
|
75
|
+
[EPOCH_STATES.BUILD_AND_SIGN_PROOF_PROPOSAL]: Object.freeze({
|
|
76
|
+
[EPOCH_EVENTS.PROPOSAL_BUILT]: EPOCH_STATES.QUORUM_DECISION,
|
|
77
|
+
}),
|
|
78
|
+
[EPOCH_STATES.QUORUM_DECISION]: Object.freeze({
|
|
79
|
+
[EPOCH_EVENTS.EXTERNAL_APPROVALS_NOT_REQUIRED]: EPOCH_STATES.BUILD_SET_EPOCH,
|
|
80
|
+
[EPOCH_EVENTS.EXTERNAL_APPROVALS_REQUIRED]: EPOCH_STATES.SEND_PROPOSAL_TO_INDEXERS,
|
|
81
|
+
[EPOCH_EVENTS.TARGET_EPOCH_ALREADY_SIGNED]: EPOCH_STATES.RELOAD_SIGNED_CONTEXT,
|
|
82
|
+
}),
|
|
83
|
+
[EPOCH_STATES.SEND_PROPOSAL_TO_INDEXERS]: Object.freeze({
|
|
84
|
+
[EPOCH_EVENTS.PROPOSAL_BROADCAST]: EPOCH_STATES.COLLECT_APPROVALS,
|
|
85
|
+
[EPOCH_EVENTS.APPROVAL_COLLECTION_FAILED]: EPOCH_STATES.BACKOFF,
|
|
86
|
+
}),
|
|
87
|
+
[EPOCH_STATES.COLLECT_APPROVALS]: Object.freeze({
|
|
88
|
+
[EPOCH_EVENTS.QUORUM_REACHED]: EPOCH_STATES.BUILD_SET_EPOCH,
|
|
89
|
+
[EPOCH_EVENTS.APPROVAL_COLLECTION_FAILED]: EPOCH_STATES.BACKOFF,
|
|
90
|
+
}),
|
|
91
|
+
[EPOCH_STATES.BUILD_SET_EPOCH]: Object.freeze({
|
|
92
|
+
[EPOCH_EVENTS.SET_EPOCH_BUILT]: EPOCH_STATES.WAIT_FOR_APPEND_REPLIES,
|
|
93
|
+
}),
|
|
94
|
+
[EPOCH_STATES.WAIT_FOR_APPEND_REPLIES]: Object.freeze({
|
|
95
|
+
[EPOCH_EVENTS.APPEND_REPLIES_COLLECTED]: EPOCH_STATES.REFRESH_SIGNED_STATE_BEFORE_APPEND,
|
|
96
|
+
[EPOCH_EVENTS.APPEND_REPLY_COLLECTION_FAILED]: EPOCH_STATES.BACKOFF,
|
|
97
|
+
[EPOCH_EVENTS.TARGET_EPOCH_ALREADY_SIGNED]: EPOCH_STATES.RELOAD_SIGNED_CONTEXT,
|
|
98
|
+
}),
|
|
99
|
+
[EPOCH_STATES.REFRESH_SIGNED_STATE_BEFORE_APPEND]: Object.freeze({
|
|
100
|
+
[EPOCH_EVENTS.TARGET_EPOCH_ABSENT]: EPOCH_STATES.APPEND_SET_EPOCH,
|
|
101
|
+
[EPOCH_EVENTS.TARGET_EPOCH_ALREADY_SIGNED]: EPOCH_STATES.RELOAD_SIGNED_CONTEXT,
|
|
102
|
+
[EPOCH_EVENTS.INDEXERS_CHANGED]: EPOCH_STATES.QUORUM_DECISION,
|
|
103
|
+
}),
|
|
104
|
+
[EPOCH_STATES.APPEND_SET_EPOCH]: Object.freeze({
|
|
105
|
+
[EPOCH_EVENTS.APPEND_STARTED]: EPOCH_STATES.WAIT_FOR_APPEND_OUTCOME,
|
|
106
|
+
[EPOCH_EVENTS.APPEND_REJECTED]: EPOCH_STATES.BACKOFF,
|
|
107
|
+
[EPOCH_EVENTS.TARGET_EPOCH_CONFIRMED]: EPOCH_STATES.RELOAD_SIGNED_CONTEXT,
|
|
108
|
+
[EPOCH_EVENTS.TARGET_EPOCH_ALREADY_SIGNED]: EPOCH_STATES.RELOAD_SIGNED_CONTEXT,
|
|
109
|
+
}),
|
|
110
|
+
[EPOCH_STATES.WAIT_FOR_APPEND_OUTCOME]: Object.freeze({
|
|
111
|
+
[EPOCH_EVENTS.TARGET_EPOCH_CONFIRMED]: EPOCH_STATES.RELOAD_SIGNED_CONTEXT,
|
|
112
|
+
[EPOCH_EVENTS.APPEND_REJECTED]: EPOCH_STATES.BACKOFF,
|
|
113
|
+
[EPOCH_EVENTS.APPENDER_SELECTION_EXPIRED]: EPOCH_STATES.BACKOFF,
|
|
114
|
+
[EPOCH_EVENTS.TARGET_EPOCH_ALREADY_SIGNED]: EPOCH_STATES.RELOAD_SIGNED_CONTEXT,
|
|
115
|
+
}),
|
|
116
|
+
[EPOCH_STATES.REFRESH_SIGNED_STATE]: Object.freeze({
|
|
117
|
+
[EPOCH_EVENTS.EPOCH_UNCHANGED_WITHOUT_LOCAL_PROOF]: EPOCH_STATES.INITIALIZE_VDF,
|
|
118
|
+
[EPOCH_EVENTS.EPOCH_UNCHANGED_WITH_LOCAL_PROOF]: EPOCH_STATES.BUILD_AND_SIGN_PROOF_PROPOSAL,
|
|
119
|
+
[EPOCH_EVENTS.NEW_EPOCH_DISCOVERED]: EPOCH_STATES.RELOAD_SIGNED_CONTEXT,
|
|
120
|
+
}),
|
|
121
|
+
[EPOCH_STATES.BACKOFF]: Object.freeze({
|
|
122
|
+
[EPOCH_EVENTS.BACKOFF_ELAPSED]: EPOCH_STATES.REFRESH_SIGNED_STATE,
|
|
123
|
+
}),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Defines the states and transitions for one epoch round. EpochCoordinationRound supplies the
|
|
128
|
+
* handlers. Proof approvals, appender selection and append confirmation are separate steps.
|
|
129
|
+
* WAIT_FOR_APPEND_OUTCOME waits for a signed result or selection expiry after the local append starts.
|
|
130
|
+
*/
|
|
131
|
+
export class EpochStateMachine extends StateMachine {
|
|
132
|
+
/**
|
|
133
|
+
* Connects the round handlers to the transition table. Entering LOAD_EPOCH_CONTEXT clears the
|
|
134
|
+
* previous context.
|
|
135
|
+
*
|
|
136
|
+
* @param {Object<string, function(EpochRoundContext, EpochStateMachine): Promise<void>>} [handlers={}] Handlers indexed by EPOCH_STATES.
|
|
137
|
+
*/
|
|
138
|
+
constructor(handlers = {}) {
|
|
139
|
+
super(TRANSITIONS, EPOCH_STATES.LOAD_EPOCH_CONTEXT, EPOCH_STATES.LOAD_EPOCH_CONTEXT, handlers);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { VDFService } from './VDFService.js';
|
|
2
|
+
import { VDFProgressWatchdog } from './VDFProgressWatchdog.js';
|
|
3
|
+
|
|
4
|
+
let bareBinaryPathPromise = null;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Finds the bundled Bare executable and saves its path for later calls. Pear needs the actual file
|
|
8
|
+
* path to start this worker. Sets the executable permission before using it.
|
|
9
|
+
*
|
|
10
|
+
* @returns {Promise<string>} Absolute executable path.
|
|
11
|
+
*/
|
|
12
|
+
async function resolveBareBinaryPath() {
|
|
13
|
+
if (!bareBinaryPathPromise) {
|
|
14
|
+
bareBinaryPathPromise = (async () => {
|
|
15
|
+
const { fileURLToPath } = await import('bare-url');
|
|
16
|
+
const fs = await import('bare-fs');
|
|
17
|
+
|
|
18
|
+
const nodeModulesUrl = typeof globalThis.Pear !== 'undefined'
|
|
19
|
+
? `file://${globalThis.Pear.config.dir}/node_modules/`
|
|
20
|
+
: new URL('../../../../node_modules/', import.meta.url).href;
|
|
21
|
+
|
|
22
|
+
const binaryPath = fileURLToPath(
|
|
23
|
+
new URL(`./bare-runtime-${Bare.platform}-${Bare.arch}/bin/bare`, nodeModulesUrl) // eslint-disable-line no-undef
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// npm doesn't reliably preserve the executable bit on this binary across installs.
|
|
27
|
+
await fs.promises.chmod(binaryPath, 0o755);
|
|
28
|
+
|
|
29
|
+
return binaryPath;
|
|
30
|
+
})();
|
|
31
|
+
}
|
|
32
|
+
return bareBinaryPathPromise;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Runs each VDF calculation in a separate Bare process. This lets the parent stop the solver by
|
|
37
|
+
* closing the process. Requests and responses use one JSON object per line. The service uses the same
|
|
38
|
+
* write, read and close methods as the Node worker.
|
|
39
|
+
*/
|
|
40
|
+
class SubprocessPort {
|
|
41
|
+
#workerScriptUrl;
|
|
42
|
+
#workerProcess = null;
|
|
43
|
+
#workerExitPromise = null;
|
|
44
|
+
#workerResponsePromise = null;
|
|
45
|
+
#resolvePendingResponse = null;
|
|
46
|
+
#closed = false;
|
|
47
|
+
|
|
48
|
+
/** @param {string} workerFileUrl file URL of the VDF worker script */
|
|
49
|
+
constructor(workerFileUrl) {
|
|
50
|
+
this.#workerScriptUrl = workerFileUrl;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Starts a Bare process and sends the calculation to its standard input. Checks whether the port
|
|
55
|
+
* was closed during setup before starting the process.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} calculationRequest Worker calculation data.
|
|
58
|
+
* @param {Buffer} calculationRequest.challenge Encoded epoch challenge.
|
|
59
|
+
* @param {number} calculationRequest.difficulty VDF iteration count.
|
|
60
|
+
* @param {number} calculationRequest.discriminantSizeBits Discriminant size in bits.
|
|
61
|
+
* @returns {Promise<void>} Resolves after sending the request. Call read() for the result.
|
|
62
|
+
*/
|
|
63
|
+
async write({ challenge, difficulty, discriminantSizeBits }) {
|
|
64
|
+
if (this.#closed) throw new Error('VDF subprocess is closed');
|
|
65
|
+
|
|
66
|
+
const { spawn } = await import('bare-subprocess');
|
|
67
|
+
const { fileURLToPath } = await import('bare-url');
|
|
68
|
+
const { default: hrtime } = await import('bare-hrtime');
|
|
69
|
+
|
|
70
|
+
const bareBinaryPath = await resolveBareBinaryPath();
|
|
71
|
+
if (this.#closed) throw new Error('VDF subprocess is closed');
|
|
72
|
+
|
|
73
|
+
const workerProcess = spawn(
|
|
74
|
+
bareBinaryPath,
|
|
75
|
+
[fileURLToPath(new URL(this.#workerScriptUrl))],
|
|
76
|
+
{ stdio: ['pipe', 'pipe', 'pipe', 'pipe'] }
|
|
77
|
+
);
|
|
78
|
+
this.#workerProcess = workerProcess;
|
|
79
|
+
|
|
80
|
+
let resolveChildExit;
|
|
81
|
+
this.#workerExitPromise = new Promise(resolve => {
|
|
82
|
+
resolveChildExit = resolve;
|
|
83
|
+
});
|
|
84
|
+
const workerPipesClosed = Promise.all(workerProcess.stdio.map(pipe => (
|
|
85
|
+
new Promise(resolve => pipe.once('close', resolve))
|
|
86
|
+
)));
|
|
87
|
+
|
|
88
|
+
// Attached synchronously, right after spawn, in the same tick as the write below -
|
|
89
|
+
// no await/gap in between where a fast response or an unexpected early exit could
|
|
90
|
+
// fire before a listener exists to catch it.
|
|
91
|
+
this.#workerResponsePromise = new Promise(resolve => {
|
|
92
|
+
let responseBuffer = '';
|
|
93
|
+
let settled = false;
|
|
94
|
+
let watchdog;
|
|
95
|
+
|
|
96
|
+
const resolveWorkerResponse = (workerResponse) => {
|
|
97
|
+
if (settled) return;
|
|
98
|
+
settled = true;
|
|
99
|
+
watchdog?.stop();
|
|
100
|
+
resolve(workerResponse);
|
|
101
|
+
// Deferred to the next tick: killing the child synchronously here, in the
|
|
102
|
+
// same tick as resolve(), was empirically linked to a downstream deadlock
|
|
103
|
+
// elsewhere in the process (unrelated pending autobase/hyperbee work never
|
|
104
|
+
// resolving) - reproduced and fixed by decoupling the kill from the resolve.
|
|
105
|
+
setImmediate(() => workerProcess.kill('SIGKILL'));
|
|
106
|
+
};
|
|
107
|
+
this.#resolvePendingResponse = resolveWorkerResponse;
|
|
108
|
+
watchdog = new VDFProgressWatchdog({
|
|
109
|
+
now: () => Number(hrtime.bigint() / 1_000_000n),
|
|
110
|
+
onStall: error => resolveWorkerResponse({ error }),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
let progressBuffer = '';
|
|
114
|
+
const ownership = workerProcess.stdio[3];
|
|
115
|
+
ownership.on('data', chunk => {
|
|
116
|
+
if (settled) return;
|
|
117
|
+
progressBuffer += chunk.toString();
|
|
118
|
+
try {
|
|
119
|
+
if (progressBuffer.length > 64 * 1024) throw new Error('VDF progress message is too large');
|
|
120
|
+
let newline;
|
|
121
|
+
while ((newline = progressBuffer.indexOf('\n')) !== -1) {
|
|
122
|
+
const message = JSON.parse(progressBuffer.slice(0, newline));
|
|
123
|
+
progressBuffer = progressBuffer.slice(newline + 1);
|
|
124
|
+
if (message.ready === true) watchdog.ready();
|
|
125
|
+
else watchdog.progress(message.progress);
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
resolveWorkerResponse({ error });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
ownership.on('end', () => resolveWorkerResponse({ error: 'VDF guardian disconnected' }));
|
|
132
|
+
|
|
133
|
+
for (const pipe of workerProcess.stdio) {
|
|
134
|
+
pipe.on('error', error => resolveWorkerResponse({ error }));
|
|
135
|
+
}
|
|
136
|
+
// An unread stderr pipe can block the worker when its buffer fills.
|
|
137
|
+
workerProcess.stderr.resume();
|
|
138
|
+
|
|
139
|
+
workerProcess.stdout.on('data', (chunk) => {
|
|
140
|
+
if (settled) return;
|
|
141
|
+
responseBuffer += chunk.toString();
|
|
142
|
+
const newlineIndex = responseBuffer.indexOf('\n');
|
|
143
|
+
if (newlineIndex === -1) return;
|
|
144
|
+
const responseLine = responseBuffer.slice(0, newlineIndex);
|
|
145
|
+
try {
|
|
146
|
+
const encodedResponse = JSON.parse(responseLine);
|
|
147
|
+
resolveWorkerResponse(decodeWorkerResponse(encodedResponse));
|
|
148
|
+
} catch {
|
|
149
|
+
// Not a Bare.Thread/bare-subprocess assumption - stdout is a boundary to an
|
|
150
|
+
// external process, so unexpected content there (e.g. a CLI usage error
|
|
151
|
+
// from spawning the wrong binary, as happened in manual QA) must fail the
|
|
152
|
+
// request instead of crashing the caller.
|
|
153
|
+
resolveWorkerResponse({ error: `VDF worker returned an invalid response: ${responseLine}` });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
workerProcess.on('exit', (exitCode, signalCode) => {
|
|
159
|
+
resolveWorkerResponse({ error: `VDF worker process exited unexpectedly (code ${exitCode}, signal ${signalCode})` });
|
|
160
|
+
// bare-subprocess does not close every pipe when the process exits.
|
|
161
|
+
// Await their native handles too, before allowing another calculation.
|
|
162
|
+
for (const pipe of workerProcess.stdio) pipe.destroy();
|
|
163
|
+
workerPipesClosed.then(resolveChildExit);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
workerProcess.stdin.write(JSON.stringify({
|
|
169
|
+
challenge: challenge.toString('hex'),
|
|
170
|
+
difficulty,
|
|
171
|
+
discriminantSizeBits
|
|
172
|
+
}) + '\n');
|
|
173
|
+
} catch (error) {
|
|
174
|
+
this.#resolvePendingResponse?.({ error });
|
|
175
|
+
// read() still waits for process and pipe cleanup before the queue advances.
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Waits for the response and for the process to exit. Clears the references only if they still
|
|
181
|
+
* belong to this calculation.
|
|
182
|
+
*
|
|
183
|
+
* @returns {Promise<import('./VDFService.js').VdfWorkerResponse>}
|
|
184
|
+
*/
|
|
185
|
+
async read() {
|
|
186
|
+
const workerResponse = this.#workerResponsePromise;
|
|
187
|
+
const workerExitPromise = this.#workerExitPromise;
|
|
188
|
+
if (!workerResponse) return { error: 'No pending VDF request' };
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const calculationResponse = await workerResponse;
|
|
192
|
+
await workerExitPromise;
|
|
193
|
+
return calculationResponse;
|
|
194
|
+
} finally {
|
|
195
|
+
if (this.#workerResponsePromise === workerResponse) {
|
|
196
|
+
this.#workerProcess = null;
|
|
197
|
+
this.#workerExitPromise = null;
|
|
198
|
+
this.#workerResponsePromise = null;
|
|
199
|
+
this.#resolvePendingResponse = null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Returns an error to the pending calculation, stops the process and waits for it to exit.
|
|
206
|
+
*
|
|
207
|
+
* @param {Error} error cancellation reason returned to the waiting calculation
|
|
208
|
+
* @returns {Promise<void>}
|
|
209
|
+
*/
|
|
210
|
+
async close(error = new Error('VDF subprocess closed')) {
|
|
211
|
+
this.#closed = true;
|
|
212
|
+
|
|
213
|
+
const workerProcess = this.#workerProcess;
|
|
214
|
+
const workerExitPromise = this.#workerExitPromise;
|
|
215
|
+
const workerResponse = this.#workerResponsePromise;
|
|
216
|
+
|
|
217
|
+
this.#resolvePendingResponse?.({ error });
|
|
218
|
+
workerProcess?.kill('SIGKILL');
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
await Promise.all([workerResponse, workerExitPromise]);
|
|
222
|
+
} finally {
|
|
223
|
+
if (this.#workerProcess === workerProcess) this.#workerProcess = null;
|
|
224
|
+
if (this.#workerExitPromise === workerExitPromise) this.#workerExitPromise = null;
|
|
225
|
+
if (this.#workerResponsePromise === workerResponse) this.#workerResponsePromise = null;
|
|
226
|
+
this.#resolvePendingResponse = null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Converts the challenge and solution in the worker response from hex to buffers.
|
|
233
|
+
*
|
|
234
|
+
* @param {object} encodedResponse Parsed JSON worker response.
|
|
235
|
+
* @returns {import('./VDFService.js').VdfWorkerResponse}
|
|
236
|
+
*/
|
|
237
|
+
function decodeWorkerResponse(encodedResponse) {
|
|
238
|
+
if (encodedResponse.error) return { error: encodedResponse.error };
|
|
239
|
+
const { challenge, difficulty, discriminantSizeBits, solution } = encodedResponse.result;
|
|
240
|
+
return {
|
|
241
|
+
result: {
|
|
242
|
+
challenge: Buffer.from(challenge, 'hex'),
|
|
243
|
+
difficulty,
|
|
244
|
+
discriminantSizeBits,
|
|
245
|
+
solution: Buffer.from(solution, 'hex')
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Runs VDF calculations in Bare and Pear using separate processes. Messages use JSON between processes
|
|
252
|
+
* and buffers inside the consensus module.
|
|
253
|
+
*/
|
|
254
|
+
export class VDFBare extends VDFService {
|
|
255
|
+
#workerURL; // for test injection
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Creates the Bare VDF service. A different worker URL can be provided for tests.
|
|
259
|
+
*
|
|
260
|
+
* @param {URL} workerURL Script to run in the worker process.
|
|
261
|
+
*/
|
|
262
|
+
constructor(workerURL = new URL('./vdf-worker.js', import.meta.url)) {
|
|
263
|
+
super();
|
|
264
|
+
this.#workerURL = workerURL;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Creates the worker port used by VDFService. */
|
|
268
|
+
async _open() {
|
|
269
|
+
this._setPort(new SubprocessPort(this.#resolveWorkerFileUrl()));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Returns the worker file URL that Bare or Pear can open.
|
|
274
|
+
*
|
|
275
|
+
* @returns {string}
|
|
276
|
+
*/
|
|
277
|
+
#resolveWorkerFileUrl() {
|
|
278
|
+
// Pear's `pear://dev/` module protocol isn't registered inside a freshly
|
|
279
|
+
// spawned subprocess, so a pear:// reference (e.g. import.meta.url under
|
|
280
|
+
// `pear run`) fails with UNKNOWN_PROTOCOL there. Pear.config.dir is the
|
|
281
|
+
// real on-disk path this app runs from (trac-msb is always run via a
|
|
282
|
+
// local directory link, not a seeded pear:// app), so build a plain
|
|
283
|
+
// file:// URL from it instead. Outside Pear (plain `bare`), the injected/default
|
|
284
|
+
// worker URL is already a file:// URL and works as-is.
|
|
285
|
+
if (typeof globalThis.Pear !== 'undefined') {
|
|
286
|
+
const applicationDirectory = globalThis.Pear.config.dir;
|
|
287
|
+
return new URL('./vdf-worker.js', `file://${applicationDirectory}/src/core/consensus/services/`).href;
|
|
288
|
+
}
|
|
289
|
+
return this.#workerURL.href;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { VDFService } from './VDFService.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runs VDF calculations in a Node worker thread. Provides the write, read and close methods used by
|
|
5
|
+
* VDFService.
|
|
6
|
+
*/
|
|
7
|
+
export class VDFNode extends VDFService {
|
|
8
|
+
#workerThread = null;
|
|
9
|
+
#workerURL; // for test injection
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates the Node VDF service. A different worker URL can be provided for tests.
|
|
13
|
+
*
|
|
14
|
+
* @param {URL} workerURL Script to run in the worker thread.
|
|
15
|
+
*/
|
|
16
|
+
constructor (workerURL = new URL('./vdf-worker.js', import.meta.url)) {
|
|
17
|
+
super();
|
|
18
|
+
this.#workerURL = workerURL;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Starts a worker thread and connects its MessagePort to VDFService.
|
|
23
|
+
*
|
|
24
|
+
* @returns {Promise<void>}
|
|
25
|
+
* @protected
|
|
26
|
+
*/
|
|
27
|
+
async _open() {
|
|
28
|
+
const { Worker, MessageChannel } = await import('worker_threads');
|
|
29
|
+
const { port1, port2 } = new MessageChannel();
|
|
30
|
+
const workerPortAdapter = this.#createWorkerPortAdapter(port1);
|
|
31
|
+
this._setPort(workerPortAdapter);
|
|
32
|
+
this.#workerThread = new Worker(this.#workerURL, {
|
|
33
|
+
workerData: { port: port2 },
|
|
34
|
+
transferList: [port2],
|
|
35
|
+
});
|
|
36
|
+
this.#workerThread.on('error', (workerError) => workerPortAdapter.fail(workerError));
|
|
37
|
+
this.#workerThread.on('exit', (exitCode) => {
|
|
38
|
+
workerPortAdapter.fail(new Error(`VDF worker exited with code ${exitCode}`));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Cancels pending reads and stops the worker thread.
|
|
44
|
+
*
|
|
45
|
+
* @returns {Promise<void>}
|
|
46
|
+
* @protected
|
|
47
|
+
*/
|
|
48
|
+
async _close() {
|
|
49
|
+
await super._close();
|
|
50
|
+
await this.#workerThread?.terminate();
|
|
51
|
+
this.#workerThread = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Keeps worker responses until read() is called. If the worker fails or closes, returns the same
|
|
56
|
+
* error to all pending reads and clears stored responses.
|
|
57
|
+
*
|
|
58
|
+
* @param {import('worker_threads').MessagePort} messagePort Node message port used by the parent process.
|
|
59
|
+
* @returns {import('./VDFService.js').VdfWorkerPort & {fail: function(Error): void}} Worker port with a callback for reporting worker errors.
|
|
60
|
+
*/
|
|
61
|
+
#createWorkerPortAdapter(messagePort) {
|
|
62
|
+
const bufferedResponses = [];
|
|
63
|
+
const pendingReaders = [];
|
|
64
|
+
let closed = false;
|
|
65
|
+
let closeError = null;
|
|
66
|
+
|
|
67
|
+
const failPendingReads = (error) => {
|
|
68
|
+
if (closed) return;
|
|
69
|
+
|
|
70
|
+
closed = true;
|
|
71
|
+
closeError = error instanceof Error ? error : new Error(String(error));
|
|
72
|
+
bufferedResponses.length = 0;
|
|
73
|
+
for (const resolve of pendingReaders) resolve({ error: closeError });
|
|
74
|
+
pendingReaders.length = 0;
|
|
75
|
+
messagePort.close();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
messagePort.on('message', (workerResponse) => {
|
|
79
|
+
if (closed) return;
|
|
80
|
+
|
|
81
|
+
if (pendingReaders.length > 0) {
|
|
82
|
+
pendingReaders.shift()(workerResponse);
|
|
83
|
+
} else {
|
|
84
|
+
bufferedResponses.push(workerResponse);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
messagePort.on('close', () => failPendingReads(new Error('VDF worker port closed')));
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
write: (calculationRequest) => {
|
|
91
|
+
if (closed) return Promise.reject(closeError);
|
|
92
|
+
messagePort.postMessage(calculationRequest);
|
|
93
|
+
return Promise.resolve();
|
|
94
|
+
},
|
|
95
|
+
read: () => new Promise(resolve => {
|
|
96
|
+
if (bufferedResponses.length > 0) {
|
|
97
|
+
resolve(bufferedResponses.shift());
|
|
98
|
+
} else if (closed) {
|
|
99
|
+
resolve({ error: closeError });
|
|
100
|
+
} else {
|
|
101
|
+
pendingReaders.push(resolve);
|
|
102
|
+
}
|
|
103
|
+
}),
|
|
104
|
+
close: (error = new Error('VDF worker closed')) => {
|
|
105
|
+
failPendingReads(error);
|
|
106
|
+
return Promise.resolve();
|
|
107
|
+
},
|
|
108
|
+
fail: failPendingReads,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// These counters describe completed work, not timer heartbeats. Keep the order in
|
|
2
|
+
// sync between the solver thread and its guardian.
|
|
3
|
+
export const VDF_PROGRESS_PHASES = [
|
|
4
|
+
'discriminant-sieve', 'discriminant-candidates', 'squaring', 'challenge-prime',
|
|
5
|
+
'proof-setup', 'proof-blocks', 'proof-aggregation', 'serialization',
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
const MIN_PROGRESS_TIMEOUT_MS = 30_000;
|
|
9
|
+
|
|
10
|
+
/** Detects silence without imposing a deadline on the complete calculation. */
|
|
11
|
+
export class VDFProgressWatchdog {
|
|
12
|
+
#now;
|
|
13
|
+
#onStall;
|
|
14
|
+
#timer;
|
|
15
|
+
#lastProgress;
|
|
16
|
+
#longestGap = 0;
|
|
17
|
+
#counts = VDF_PROGRESS_PHASES.map(() => 0n);
|
|
18
|
+
#stopped = false;
|
|
19
|
+
#phase = 'startup';
|
|
20
|
+
#ready = false;
|
|
21
|
+
|
|
22
|
+
constructor({ now, onStall }) {
|
|
23
|
+
this.#now = now;
|
|
24
|
+
this.#onStall = onStall;
|
|
25
|
+
this.#lastProgress = now();
|
|
26
|
+
// Before the first observation there is no measured pace. This is a
|
|
27
|
+
// startup allowance, not a limit derived from VDF difficulty or length.
|
|
28
|
+
this.#arm(MIN_PROGRESS_TIMEOUT_MS);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
ready() {
|
|
32
|
+
if (this.#ready || this.#stopped) return;
|
|
33
|
+
this.#ready = true;
|
|
34
|
+
// Process/thread startup is separate from the native solver's pace.
|
|
35
|
+
this.#lastProgress = this.#now();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
progress(counts) {
|
|
39
|
+
if (this.#stopped) return;
|
|
40
|
+
if (!Array.isArray(counts) || counts.length !== this.#counts.length) {
|
|
41
|
+
throw new Error('Invalid VDF progress counters');
|
|
42
|
+
}
|
|
43
|
+
const next = counts.map(count => {
|
|
44
|
+
if (typeof count !== 'string' || !/^\d{1,20}$/.test(count)) {
|
|
45
|
+
throw new Error('Invalid VDF progress counter');
|
|
46
|
+
}
|
|
47
|
+
return BigInt(count);
|
|
48
|
+
});
|
|
49
|
+
let advanced = false;
|
|
50
|
+
for (let i = 0; i < next.length; i++) {
|
|
51
|
+
if (next[i] < this.#counts[i]) throw new Error('VDF progress moved backwards');
|
|
52
|
+
if (next[i] > this.#counts[i]) {
|
|
53
|
+
advanced = true;
|
|
54
|
+
this.#phase = VDF_PROGRESS_PHASES[i];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!advanced) return;
|
|
58
|
+
const now = this.#now();
|
|
59
|
+
this.#longestGap = Math.max(this.#longestGap, now - this.#lastProgress);
|
|
60
|
+
this.#lastProgress = now;
|
|
61
|
+
this.#counts = next;
|
|
62
|
+
this.#arm(Math.max(MIN_PROGRESS_TIMEOUT_MS, this.#longestGap * 8));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
stop() {
|
|
66
|
+
this.#stopped = true;
|
|
67
|
+
clearTimeout(this.#timer);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#arm(delay) {
|
|
71
|
+
clearTimeout(this.#timer);
|
|
72
|
+
this.#timer = setTimeout(() => {
|
|
73
|
+
this.stop();
|
|
74
|
+
const error = new Error(`VDF stopped making progress (${this.#phase})`);
|
|
75
|
+
error.code = 'VDF_STALLED';
|
|
76
|
+
this.#onStall(error);
|
|
77
|
+
}, Math.min(delay, 2_147_483_647));
|
|
78
|
+
}
|
|
79
|
+
}
|