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
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import ConsensusValidationSchema from "./ConsensusValidationSchema.js";
|
|
2
|
+
import _ from "lodash";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ConsensusVersion,
|
|
6
|
+
ConsensusOperationType,
|
|
7
|
+
ConsensusResultCode
|
|
8
|
+
} from "../../../../../../utils/constants.js";
|
|
9
|
+
import {V1ConsensusProtocolError} from "../V1ConsensusProtocolError.js";
|
|
10
|
+
import tracCryptoApi from "trac-crypto-api";
|
|
11
|
+
import {createMessage} from "../../../../../../utils/buffer.js";
|
|
12
|
+
import {bufferToAddress} from "../../../../../state/utils/address.js";
|
|
13
|
+
import b4a from "b4a";
|
|
14
|
+
import { appendSignatureData, fromAppendIntent } from '../../../../../../messages/consensus/v1/epochAppendMessage.js';
|
|
15
|
+
import { sameSelectionContext } from '../../../../../consensus/epochAppenderSelection/epochAppenderSelectionState.js';
|
|
16
|
+
|
|
17
|
+
class V1BaseConsensusOperation {
|
|
18
|
+
#consensusValidationSchema;
|
|
19
|
+
_config;
|
|
20
|
+
_state;
|
|
21
|
+
/**
|
|
22
|
+
* Creates the base validator shared by consensus v1 operations.
|
|
23
|
+
*
|
|
24
|
+
* Initializes operation schemas and stores the shared dependencies used by
|
|
25
|
+
* address, signature, and state-backed validation.
|
|
26
|
+
*
|
|
27
|
+
* @param {Config} config Application configuration. The base validator uses
|
|
28
|
+
* `addressLength` for schemas and `addressPrefix` for address conversion.
|
|
29
|
+
* @param {State} state Ledger state. It must expose `isIndexerAddress(address)`
|
|
30
|
+
* and `requireSignedConsensusConfig()` for membership and consensus validation.
|
|
31
|
+
* @throws {Error} When consensus schemas cannot be initialized from the configuration.
|
|
32
|
+
*/
|
|
33
|
+
constructor(config, state) {
|
|
34
|
+
this.#consensusValidationSchema = new ConsensusValidationSchema(config);
|
|
35
|
+
this._config = config;
|
|
36
|
+
this._state = state;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Runs a validator behind the consensus protocol error boundary.
|
|
41
|
+
*
|
|
42
|
+
* Expected protocol errors keep their result code and identity. Unexpected
|
|
43
|
+
* errors from state, codecs, crypto APIs, or malformed runtime values are
|
|
44
|
+
* exposed as a stable protocol error while remaining available as `cause`.
|
|
45
|
+
* Validation operations are expected to throw `Error` instances.
|
|
46
|
+
*
|
|
47
|
+
* @param {function(): (Promise<*>|*)} validation Validation operation to execute.
|
|
48
|
+
* @returns {Promise<*>} Result returned by the validation operation.
|
|
49
|
+
* @throws {V1ConsensusProtocolError} Re-throws protocol errors or wraps unexpected failures.
|
|
50
|
+
*/
|
|
51
|
+
async validateAsProtocolError(validation) {
|
|
52
|
+
try {
|
|
53
|
+
return await validation();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error instanceof V1ConsensusProtocolError) {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const protocolError = new V1ConsensusProtocolError(
|
|
60
|
+
ConsensusResultCode.UNEXPECTED_ERROR,
|
|
61
|
+
error.message
|
|
62
|
+
);
|
|
63
|
+
protocolError.cause = error;
|
|
64
|
+
throw protocolError;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validates the payload against the schema selected by its operation type.
|
|
70
|
+
*
|
|
71
|
+
* Checks that the operation type is present, supported, and that all fields
|
|
72
|
+
* required by the corresponding request or approval schema are valid.
|
|
73
|
+
*
|
|
74
|
+
* @param {object} payload Decoded consensus operation payload.
|
|
75
|
+
* @returns {void}
|
|
76
|
+
* @throws {V1ConsensusProtocolError} When the type or payload schema is invalid.
|
|
77
|
+
*/
|
|
78
|
+
isPayloadSchemaValid(payload) {
|
|
79
|
+
if (_.isNil(payload?.type)) {
|
|
80
|
+
throw new V1ConsensusProtocolError(
|
|
81
|
+
ConsensusResultCode.INVALID_PAYLOAD,
|
|
82
|
+
'Payload or payload type is missing.'
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const selectedValidator = this.#selectCheckSchemaValidator(payload.type);
|
|
87
|
+
const isPayloadValid = selectedValidator(payload);
|
|
88
|
+
if (!isPayloadValid) {
|
|
89
|
+
throw new V1ConsensusProtocolError(
|
|
90
|
+
ConsensusResultCode.SCHEMA_VALIDATION_FAILED,
|
|
91
|
+
'Payload is invalid.'
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Requires active VDF V1 consensus and matching proof parameters in signed state.
|
|
98
|
+
* Used for both proposals and approvals, including replies to pending requests.
|
|
99
|
+
*
|
|
100
|
+
* @param {object} proofProposal Decoded proof proposal.
|
|
101
|
+
* @returns {Promise<void>}
|
|
102
|
+
* @throws {V1ConsensusProtocolError} When VDF V1 is inactive or the parameters differ.
|
|
103
|
+
*/
|
|
104
|
+
async validateProofProposalConfig(proofProposal) {
|
|
105
|
+
const consensusConfig = await this._state.requireSignedConsensusConfig();
|
|
106
|
+
if (consensusConfig.schemaVersion !== ConsensusVersion.VDF_V1) {
|
|
107
|
+
throw new V1ConsensusProtocolError(
|
|
108
|
+
ConsensusResultCode.CONSENSUS_CONFIG_MISMATCH,
|
|
109
|
+
'VDF V1 is not the active consensus.'
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const difficulty = proofProposal.difficulty.readUInt32BE(0);
|
|
114
|
+
const discriminantBitSize = proofProposal.discriminant_bit_size.readUInt16BE(0);
|
|
115
|
+
if (
|
|
116
|
+
difficulty !== consensusConfig.configData.difficulty ||
|
|
117
|
+
discriminantBitSize !== consensusConfig.configData.discriminantBitSize
|
|
118
|
+
) {
|
|
119
|
+
throw new V1ConsensusProtocolError(
|
|
120
|
+
ConsensusResultCode.CONSENSUS_CONFIG_MISMATCH,
|
|
121
|
+
'Proof proposal does not match the current consensus configuration.'
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Builds canonical VDF challenge data for a consensus v1 proof proposal.
|
|
128
|
+
*
|
|
129
|
+
* Concatenates fields 1 through 6: network id, epoch, previous epoch record hash,
|
|
130
|
+
* proposer address, difficulty, and discriminant bit size.
|
|
131
|
+
*
|
|
132
|
+
* @param {object} proofProposal Decoded proof proposal.
|
|
133
|
+
* @returns {Buffer} Canonically encoded challenge data.
|
|
134
|
+
*/
|
|
135
|
+
buildProofProposalChallengeData(proofProposal) {
|
|
136
|
+
return createMessage(
|
|
137
|
+
proofProposal.network_id,
|
|
138
|
+
proofProposal.epoch,
|
|
139
|
+
proofProposal.previous_epoch_record_hash,
|
|
140
|
+
proofProposal.proposer,
|
|
141
|
+
proofProposal.difficulty,
|
|
142
|
+
proofProposal.discriminant_bit_size
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Selects the schema validator for the consensus operation type.
|
|
148
|
+
*
|
|
149
|
+
* @param {number} type Consensus operation type.
|
|
150
|
+
* @returns {function(object): boolean} Bound schema validation function.
|
|
151
|
+
* @throws {V1ConsensusProtocolError} When the operation type is not an integer or is unsupported.
|
|
152
|
+
*/
|
|
153
|
+
#selectCheckSchemaValidator(type) {
|
|
154
|
+
if (!Number.isInteger(type)) {
|
|
155
|
+
throw new V1ConsensusProtocolError(
|
|
156
|
+
ConsensusResultCode.OPERATION_TYPE_INVALID,
|
|
157
|
+
'Operation type must be an integer.'
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
switch (type) {
|
|
162
|
+
case ConsensusOperationType.REQUEST:
|
|
163
|
+
return this.#consensusValidationSchema.validateEpochAppendRequest.bind(this.#consensusValidationSchema);
|
|
164
|
+
case ConsensusOperationType.REPLY:
|
|
165
|
+
return this.#consensusValidationSchema.validateEpochAppendReply.bind(this.#consensusValidationSchema);
|
|
166
|
+
case ConsensusOperationType.UNSPECIFIED:
|
|
167
|
+
throw new V1ConsensusProtocolError(
|
|
168
|
+
ConsensusResultCode.OPERATION_TYPE_INVALID,
|
|
169
|
+
'Operation type is unspecified.'
|
|
170
|
+
);
|
|
171
|
+
case ConsensusOperationType.PROOF_PROPOSAL:
|
|
172
|
+
return this.#consensusValidationSchema.validateConsensusV1ProofProposal.bind(this.#consensusValidationSchema);
|
|
173
|
+
case ConsensusOperationType.PROOF_PROPOSAL_APPROVAL:
|
|
174
|
+
return this.#consensusValidationSchema.validateV1EpochProofProposalResponse.bind(this.#consensusValidationSchema);
|
|
175
|
+
default:
|
|
176
|
+
throw new V1ConsensusProtocolError(
|
|
177
|
+
ConsensusResultCode.OPERATION_TYPE_INVALID,
|
|
178
|
+
`Unknown operation type: ${type}`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Verifies the operation signature against the remote peer public key.
|
|
185
|
+
*
|
|
186
|
+
* Builds the canonical request or approval signature message, hashes it with
|
|
187
|
+
* BLAKE3, and verifies the signature using the supplied public key.
|
|
188
|
+
*
|
|
189
|
+
* @param {object} payload Decoded consensus operation payload.
|
|
190
|
+
* @param {Buffer} remotePublicKey Public key received from the peer connection.
|
|
191
|
+
* @param {object} [proofProposal] Original proof proposal required for approval verification.
|
|
192
|
+
* @param {number} [invalidSignatureResultCode] ConsensusResultCode to use when the signature is invalid.
|
|
193
|
+
* @returns {Promise<void>}
|
|
194
|
+
* @throws {V1ConsensusProtocolError} When message construction, hashing, or signature verification fails.
|
|
195
|
+
*/
|
|
196
|
+
async validateSignature(
|
|
197
|
+
payload,
|
|
198
|
+
remotePublicKey,
|
|
199
|
+
proofProposal,
|
|
200
|
+
invalidSignatureResultCode = ConsensusResultCode.UNEXPECTED_ERROR
|
|
201
|
+
) {
|
|
202
|
+
let signature;
|
|
203
|
+
let message;
|
|
204
|
+
try {
|
|
205
|
+
const result = this.#buildSignatureMessage(payload, proofProposal);
|
|
206
|
+
signature = result.signature;
|
|
207
|
+
message = result.message;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (error instanceof V1ConsensusProtocolError) {
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
throw new V1ConsensusProtocolError(
|
|
213
|
+
ConsensusResultCode.UNEXPECTED_ERROR,
|
|
214
|
+
`Failed to build signature message: ${error.message}`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!remotePublicKey) {
|
|
219
|
+
throw new V1ConsensusProtocolError(
|
|
220
|
+
ConsensusResultCode.UNEXPECTED_ERROR,
|
|
221
|
+
'Remote public key is missing.'
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let hash;
|
|
226
|
+
try {
|
|
227
|
+
hash = await tracCryptoApi.hash.blake3(message);
|
|
228
|
+
} catch {
|
|
229
|
+
throw new V1ConsensusProtocolError(
|
|
230
|
+
ConsensusResultCode.UNEXPECTED_ERROR,
|
|
231
|
+
'Failed to hash signature message.'
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let verified = false;
|
|
236
|
+
try {
|
|
237
|
+
verified = tracCryptoApi.signature.verify(signature, hash, remotePublicKey);
|
|
238
|
+
} catch {
|
|
239
|
+
verified = false;
|
|
240
|
+
}
|
|
241
|
+
if (!verified) {
|
|
242
|
+
throw new V1ConsensusProtocolError(
|
|
243
|
+
invalidSignatureResultCode,
|
|
244
|
+
'signature verification failed.'
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Builds the canonical signature message for the internal consensus operation.
|
|
251
|
+
*
|
|
252
|
+
* A proposal message covers the VDF challenge and proof. An approval message
|
|
253
|
+
* additionally covers the approver and the original proposal signature.
|
|
254
|
+
* Appender selection messages use their own header and vote signature framing.
|
|
255
|
+
*
|
|
256
|
+
* @param {object} payload Decoded consensus operation payload.
|
|
257
|
+
* @param {object} [proofProposalContext] Original proposal used for an approval message.
|
|
258
|
+
* @returns {{signature: Buffer, message: Buffer}} Signature and canonical message to verify.
|
|
259
|
+
* @throws {V1ConsensusProtocolError} When the operation type is invalid or unsupported.
|
|
260
|
+
*/
|
|
261
|
+
#buildSignatureMessage(payload, proofProposalContext) {
|
|
262
|
+
if (!Number.isInteger(payload.type)) {
|
|
263
|
+
throw new V1ConsensusProtocolError(
|
|
264
|
+
ConsensusResultCode.OPERATION_TYPE_INVALID,
|
|
265
|
+
'Operation type must be an integer.'
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
switch (payload.type) {
|
|
270
|
+
case ConsensusOperationType.REQUEST:
|
|
271
|
+
case ConsensusOperationType.REPLY: {
|
|
272
|
+
const body = payload.type === ConsensusOperationType.REQUEST ? payload.request : payload.reply;
|
|
273
|
+
return { signature: body.signature, message: appendSignatureData(payload) };
|
|
274
|
+
}
|
|
275
|
+
case ConsensusOperationType.UNSPECIFIED:
|
|
276
|
+
throw new V1ConsensusProtocolError(
|
|
277
|
+
ConsensusResultCode.OPERATION_TYPE_INVALID,
|
|
278
|
+
'Operation type is unspecified.'
|
|
279
|
+
);
|
|
280
|
+
case ConsensusOperationType.PROOF_PROPOSAL: {
|
|
281
|
+
const proofProposal = payload.proof_proposal;
|
|
282
|
+
const challengeData = this.buildProofProposalChallengeData(proofProposal);
|
|
283
|
+
const message = createMessage(challengeData, proofProposal.proof);
|
|
284
|
+
|
|
285
|
+
return {signature: proofProposal.signature, message};
|
|
286
|
+
}
|
|
287
|
+
case ConsensusOperationType.PROOF_PROPOSAL_APPROVAL: {
|
|
288
|
+
const proofProposal = proofProposalContext;
|
|
289
|
+
const approval = payload.proof_proposal_response.approval;
|
|
290
|
+
const challengeData = this.buildProofProposalChallengeData(proofProposal);
|
|
291
|
+
|
|
292
|
+
const message = createMessage(
|
|
293
|
+
challengeData,
|
|
294
|
+
proofProposal.proof,
|
|
295
|
+
approval.approver,
|
|
296
|
+
proofProposal.signature
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
return {signature: approval.approval_sig, message};
|
|
300
|
+
}
|
|
301
|
+
default:
|
|
302
|
+
throw new V1ConsensusProtocolError(
|
|
303
|
+
ConsensusResultCode.OPERATION_TYPE_INVALID,
|
|
304
|
+
`Unknown operation type: ${payload.type}`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Validates that a binary protocol address belongs to the connected peer.
|
|
311
|
+
*
|
|
312
|
+
* Converts the binary address to its configured textual representation,
|
|
313
|
+
* decodes its public key, and compares it with the remote connection key.
|
|
314
|
+
*
|
|
315
|
+
* @param {Buffer} binaryAddress Binary proposer or approver address from the payload.
|
|
316
|
+
* @param {Buffer} remotePublicKey Public key received from the peer connection.
|
|
317
|
+
* @returns {void}
|
|
318
|
+
* @throws {V1ConsensusProtocolError} When the address is invalid or belongs to another key.
|
|
319
|
+
*/
|
|
320
|
+
assertAddressWithRemotePublicKey(binaryAddress, remotePublicKey) {
|
|
321
|
+
const address = bufferToAddress(binaryAddress, this._config.addressPrefix);
|
|
322
|
+
if (!address) {
|
|
323
|
+
throw new V1ConsensusProtocolError(
|
|
324
|
+
ConsensusResultCode.ADDRESS_INVALID,
|
|
325
|
+
'Address is invalid.'
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
let publicKeyFromAddress;
|
|
329
|
+
try {
|
|
330
|
+
publicKeyFromAddress = tracCryptoApi.address.decode(address);
|
|
331
|
+
} catch {
|
|
332
|
+
throw new V1ConsensusProtocolError(
|
|
333
|
+
ConsensusResultCode.ADDRESS_INVALID,
|
|
334
|
+
'Address is invalid.'
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!remotePublicKey) {
|
|
339
|
+
throw new V1ConsensusProtocolError(
|
|
340
|
+
ConsensusResultCode.UNEXPECTED_ERROR,
|
|
341
|
+
'Remote public key is missing.'
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!b4a.equals(publicKeyFromAddress, remotePublicKey)) {
|
|
346
|
+
throw new V1ConsensusProtocolError(
|
|
347
|
+
ConsensusResultCode.PUBLIC_KEY_MISMATCH,
|
|
348
|
+
'Address does not match remote public key.'
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Validates that the connected peer is registered as an indexer in state.
|
|
355
|
+
*
|
|
356
|
+
* Encodes the remote public key as a configured network address and checks
|
|
357
|
+
* that address against the current indexer set.
|
|
358
|
+
*
|
|
359
|
+
* @param {Buffer} remotePublicKey Public key received from the peer connection.
|
|
360
|
+
* @returns {Promise<void>}
|
|
361
|
+
* @throws {V1ConsensusProtocolError} When the remote address is not an indexer.
|
|
362
|
+
*/
|
|
363
|
+
async validateAddressIsIndexer(remotePublicKey) {
|
|
364
|
+
// TODO: In the future, we should handle this specific error to not only drop the connection
|
|
365
|
+
// but also blacklist the specific node.
|
|
366
|
+
// Such an error would mean that someone is trying to impersonate an indexer.
|
|
367
|
+
const address = tracCryptoApi.address.encode(this._config.addressPrefix, remotePublicKey);
|
|
368
|
+
const isIndexer = await this._state.isIndexerAddress(address);
|
|
369
|
+
if (!isIndexer) {
|
|
370
|
+
throw new V1ConsensusProtocolError(
|
|
371
|
+
ConsensusResultCode.INDEXER_ROLE_INVALID,
|
|
372
|
+
'Incoming address is not an indexer.'
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Validates that this node is still an indexer before it acts on an indexer-only
|
|
379
|
+
* operation (e.g. signing a proposal approval). A node that lost its indexer role
|
|
380
|
+
* must not approve or sign anything on the indexer's behalf.
|
|
381
|
+
*
|
|
382
|
+
* @returns {void}
|
|
383
|
+
* @throws {V1ConsensusProtocolError} When the local node is not an indexer.
|
|
384
|
+
*/
|
|
385
|
+
validateLocalNodeIsIndexer() {
|
|
386
|
+
if (!this._state.isIndexer()) {
|
|
387
|
+
throw new V1ConsensusProtocolError(
|
|
388
|
+
ConsensusResultCode.INDEXER_ROLE_INVALID,
|
|
389
|
+
'Local node is not an indexer and cannot approve proposals.'
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Resolve membership locally from signed state; it is not supplied by the peer.
|
|
396
|
+
* @returns {Promise<import('../../../../../consensus/epochAppenderSelection/epochAppenderSelectionState.js').EpochAppendRequest>}
|
|
397
|
+
*/
|
|
398
|
+
async validateAppendContext(intent, remotePublicKey) {
|
|
399
|
+
const request = fromAppendIntent(intent);
|
|
400
|
+
const context = await this._state.getEpochAppenderSelectionContext();
|
|
401
|
+
const peer = b4a.toString(remotePublicKey, 'hex');
|
|
402
|
+
const includesPeer = context.members.some(member => member.publicKey === peer);
|
|
403
|
+
const includesRequester = context.members.some(member => member.publicKey === request.requester);
|
|
404
|
+
if (!sameSelectionContext(request, context) || !includesPeer || !includesRequester) {
|
|
405
|
+
throw new V1ConsensusProtocolError(ConsensusResultCode.EPOCH_INVALID, 'Stale or foreign append context');
|
|
406
|
+
}
|
|
407
|
+
return { ...request, members: context.members };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Authenticate REQUEST/REPLY before reading the signed election context. */
|
|
411
|
+
async validateAppendMessage(message, remotePublicKey, expectedType) {
|
|
412
|
+
if (message?.type !== expectedType) {
|
|
413
|
+
throw new V1ConsensusProtocolError(ConsensusResultCode.INVALID_PAYLOAD, 'Unexpected epoch append message type');
|
|
414
|
+
}
|
|
415
|
+
this.isPayloadSchemaValid(message);
|
|
416
|
+
const isRequest = expectedType === ConsensusOperationType.REQUEST;
|
|
417
|
+
const body = isRequest ? message.request : message.reply;
|
|
418
|
+
const author = isRequest ? body.intent.requester : body.responder;
|
|
419
|
+
if (!b4a.equals(author, remotePublicKey)) {
|
|
420
|
+
throw new V1ConsensusProtocolError(ConsensusResultCode.PUBLIC_KEY_MISMATCH, 'Append message author mismatch');
|
|
421
|
+
}
|
|
422
|
+
await this.validateSignature(message, remotePublicKey, undefined, ConsensusResultCode.RESPONSE_SIGNATURE_INVALID);
|
|
423
|
+
return this.validateAppendContext(body.intent, remotePublicKey);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
export default V1BaseConsensusOperation;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import V1BaseConsensusOperation from './V1BaseConsensusOperation.js';
|
|
2
|
+
import { ConsensusOperationType, ConsensusResultCode } from '../../../../../../utils/constants.js';
|
|
3
|
+
import { V1ConsensusProtocolError } from '../V1ConsensusProtocolError.js';
|
|
4
|
+
import { fromAppendIntent } from '../../../../../../messages/consensus/v1/epochAppendMessage.js';
|
|
5
|
+
import { sameSelectionRequest, sameSelectionContext, EpochAppenderVoteStatus } from '../../../../../consensus/epochAppenderSelection/epochAppenderSelectionState.js';
|
|
6
|
+
|
|
7
|
+
export default class V1EpochAppendReply extends V1BaseConsensusOperation {
|
|
8
|
+
/**
|
|
9
|
+
* Authenticate REPLY and bind it to the exact outstanding REQUEST and RPC session.
|
|
10
|
+
* @param {object} message Decoded consensus v1 reply.
|
|
11
|
+
* @param {object} connection Authenticated peer connection.
|
|
12
|
+
* @param {object} pendingRequest Original outbound message.
|
|
13
|
+
* @returns {Promise<import('../../../../../consensus/epochAppenderSelection/epochAppenderSelectionState.js').EpochAppendRequest>}
|
|
14
|
+
* @throws {V1ConsensusProtocolError}
|
|
15
|
+
*/
|
|
16
|
+
async validate(message, connection, pendingRequest) {
|
|
17
|
+
return this.validateAsProtocolError(async () => {
|
|
18
|
+
const request = await this.validateAppendMessage(message, connection.remotePublicKey, ConsensusOperationType.REPLY);
|
|
19
|
+
const body = message.reply;
|
|
20
|
+
this.#validateCorrelation(message, request, pendingRequest);
|
|
21
|
+
// A rejection may report the last vote from an older epoch/configuration.
|
|
22
|
+
// It advances the local term only; it can never count as permission.
|
|
23
|
+
const vote = fromAppendIntent(body.vote);
|
|
24
|
+
const observedCounter = body.observed_counter.readBigUInt64BE(0);
|
|
25
|
+
const requestedTerm = request.logicalCounter;
|
|
26
|
+
const votedTerm = vote.logicalCounter;
|
|
27
|
+
if (vote.networkId !== request.networkId || vote.bootstrap !== request.bootstrap ||
|
|
28
|
+
requestedTerm <= 0n || votedTerm <= 0n || observedCounter < requestedTerm || observedCounter < votedTerm ||
|
|
29
|
+
(body.vote_status === EpochAppenderVoteStatus.GRANTED && !sameSelectionRequest(request, vote)) ||
|
|
30
|
+
(body.vote_status === EpochAppenderVoteStatus.BUSY && !sameSelectionContext(request, vote)) ||
|
|
31
|
+
(body.vote_status === EpochAppenderVoteStatus.STALE && votedTerm < requestedTerm)) {
|
|
32
|
+
throw new V1ConsensusProtocolError(ConsensusResultCode.INVALID_PAYLOAD, 'Inconsistent appender vote reply');
|
|
33
|
+
}
|
|
34
|
+
return { ...request, voteStatus: body.vote_status, observedCounter, vote };
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#validateCorrelation(message, request, pendingRequest) {
|
|
39
|
+
const expectedIntent = pendingRequest?.request?.intent;
|
|
40
|
+
if (!expectedIntent || message.session_id !== pendingRequest.session_id) {
|
|
41
|
+
throw new V1ConsensusProtocolError(ConsensusResultCode.INVALID_PAYLOAD, 'Unrelated append reply session');
|
|
42
|
+
}
|
|
43
|
+
if (!sameSelectionRequest(request, fromAppendIntent(expectedIntent))) {
|
|
44
|
+
throw new V1ConsensusProtocolError(ConsensusResultCode.INVALID_PAYLOAD, 'Unrelated append reply request');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import V1BaseConsensusOperation from './V1BaseConsensusOperation.js';
|
|
2
|
+
import { ConsensusOperationType } from '../../../../../../utils/constants.js';
|
|
3
|
+
|
|
4
|
+
export default class V1EpochAppendRequest extends V1BaseConsensusOperation {
|
|
5
|
+
/**
|
|
6
|
+
* Authenticate REQUEST and resolve its signed arbitration membership.
|
|
7
|
+
* @param {object} message Decoded consensus v1 message.
|
|
8
|
+
* @param {object} connection Authenticated peer connection.
|
|
9
|
+
* @returns {Promise<import('../../../../../consensus/epochAppenderSelection/epochAppenderSelectionState.js').EpochAppendRequest>}
|
|
10
|
+
* @throws {import('../V1ConsensusProtocolError.js').V1ConsensusProtocolError}
|
|
11
|
+
*/
|
|
12
|
+
async validate(message, connection) {
|
|
13
|
+
return this.validateAsProtocolError(() =>
|
|
14
|
+
this.validateAppendMessage(message, connection.remotePublicKey, ConsensusOperationType.REQUEST)
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import V1BaseConsensusOperation from "./V1BaseConsensusOperation.js";
|
|
2
|
+
import { ConsensusResultCode } from "../../../../../../utils/constants.js";
|
|
3
|
+
import {createMessage , uint32ToBuffer} from "../../../../../../utils/buffer.js";
|
|
4
|
+
import {encodeProofProposalApproval} from "../../../../../../codecs/consensus/v1/consensusV1OperationCodec.js";
|
|
5
|
+
import tracCryptoApi from "trac-crypto-api";
|
|
6
|
+
import {V1ConsensusProtocolError} from "../V1ConsensusProtocolError.js";
|
|
7
|
+
|
|
8
|
+
class V1EpochProofProposalApproval extends V1BaseConsensusOperation {
|
|
9
|
+
/**
|
|
10
|
+
* Creates the proof proposal approval validator.
|
|
11
|
+
*
|
|
12
|
+
* @param {Config} config Application configuration. Approval validation uses
|
|
13
|
+
* `addressLength` for schemas and `addressPrefix` for approver addresses.
|
|
14
|
+
* @param {State} state Ledger state. It must expose
|
|
15
|
+
* `isIndexerAddress(address)` and `requireSignedConsensusConfig()`.
|
|
16
|
+
* @throws {Error} When consensus schemas cannot be initialized from the configuration.
|
|
17
|
+
*/
|
|
18
|
+
constructor(config, state) {
|
|
19
|
+
super(config, state);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validates an incoming proof proposal response and returns its outcome.
|
|
24
|
+
*
|
|
25
|
+
* Checks every response's schema and signature. A signed rejection is returned
|
|
26
|
+
* as a result. An OK response also requires the approver to match the remote
|
|
27
|
+
* public key, a valid approval signature, indexer membership, and matching
|
|
28
|
+
* active consensus config before and after the asynchronous approval checks.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} payload Decoded proof proposal approval payload.
|
|
31
|
+
* @param {object} connection Peer connection containing `remotePublicKey`.
|
|
32
|
+
* @param {object} proofProposal Original proof proposal being approved.
|
|
33
|
+
* @returns {Promise<{resultCode: number, approval?: object}>} Authenticated peer outcome.
|
|
34
|
+
* @throws {V1ConsensusProtocolError} When local response validation fails.
|
|
35
|
+
*/
|
|
36
|
+
async validate(payload, connection, proofProposal) {
|
|
37
|
+
return await this.validateAsProtocolError(async () => {
|
|
38
|
+
this.isPayloadSchemaValid(payload);
|
|
39
|
+
|
|
40
|
+
await this.#validateResponseSignature(payload, connection.remotePublicKey);
|
|
41
|
+
const resultCode = payload.proof_proposal_response.result;
|
|
42
|
+
if (resultCode !== ConsensusResultCode.OK) {
|
|
43
|
+
return { resultCode };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await this.validateProofProposalConfig(proofProposal);
|
|
47
|
+
const approval = payload.proof_proposal_response.approval;
|
|
48
|
+
this.assertAddressWithRemotePublicKey(
|
|
49
|
+
approval.approver,
|
|
50
|
+
connection.remotePublicKey
|
|
51
|
+
);
|
|
52
|
+
await this.validateSignature(payload, connection.remotePublicKey, proofProposal, ConsensusResultCode.APPROVAL_SIGNATURE_INVALID);
|
|
53
|
+
await this.validateAddressIsIndexer(connection.remotePublicKey);
|
|
54
|
+
|
|
55
|
+
// Reject late approvals if the signed config changed while validating them.
|
|
56
|
+
await this.validateProofProposalConfig(proofProposal);
|
|
57
|
+
return { resultCode, approval };
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Verifies the response signature over result code and optional encoded approval.
|
|
63
|
+
*
|
|
64
|
+
* For an OK response the signed message covers both result code and encoded
|
|
65
|
+
* approval. For a rejection it covers only the result code.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} payload Decoded proof proposal response payload.
|
|
68
|
+
* @param {Buffer} remotePublicKey Public key received from the peer connection.
|
|
69
|
+
* @returns {Promise<void>}
|
|
70
|
+
* @throws {V1ConsensusProtocolError} When hashing or response signature verification fails.
|
|
71
|
+
*/
|
|
72
|
+
async #validateResponseSignature(payload, remotePublicKey) {
|
|
73
|
+
const proofProposalResponse = payload.proof_proposal_response;
|
|
74
|
+
const resultCode = uint32ToBuffer(proofProposalResponse.result);
|
|
75
|
+
const message = proofProposalResponse.result === ConsensusResultCode.OK
|
|
76
|
+
? createMessage(resultCode, encodeProofProposalApproval(proofProposalResponse.approval))
|
|
77
|
+
: createMessage(resultCode);
|
|
78
|
+
|
|
79
|
+
let hash;
|
|
80
|
+
try {
|
|
81
|
+
hash = await tracCryptoApi.hash.blake3(message);
|
|
82
|
+
} catch {
|
|
83
|
+
throw new V1ConsensusProtocolError(
|
|
84
|
+
ConsensusResultCode.UNEXPECTED_ERROR,
|
|
85
|
+
'Failed to hash response signature message.'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!remotePublicKey) {
|
|
90
|
+
throw new V1ConsensusProtocolError(
|
|
91
|
+
ConsensusResultCode.UNEXPECTED_ERROR,
|
|
92
|
+
'Remote public key is missing.'
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let verified;
|
|
97
|
+
try {
|
|
98
|
+
verified = tracCryptoApi.signature.verify(
|
|
99
|
+
proofProposalResponse.response_sig,
|
|
100
|
+
hash,
|
|
101
|
+
remotePublicKey
|
|
102
|
+
);
|
|
103
|
+
} catch {
|
|
104
|
+
verified = false;
|
|
105
|
+
}
|
|
106
|
+
if (!verified) {
|
|
107
|
+
throw new V1ConsensusProtocolError(
|
|
108
|
+
ConsensusResultCode.RESPONSE_SIGNATURE_INVALID,
|
|
109
|
+
'response signature verification failed.'
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default V1EpochProofProposalApproval;
|