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/state/State.js
CHANGED
|
@@ -5,43 +5,87 @@ import b4a from 'b4a';
|
|
|
5
5
|
import {
|
|
6
6
|
ACK_INTERVAL,
|
|
7
7
|
ADMIN_INITIAL_BALANCE,
|
|
8
|
-
|
|
9
|
-
OperationType,
|
|
8
|
+
ADMIN_INITIAL_STAKED_BALANCE,
|
|
10
9
|
AUTOBASE_VALUE_ENCODING,
|
|
10
|
+
BATCH_SIZE,
|
|
11
|
+
CONSENSUS_CONFIG_INDEX_SIZE,
|
|
12
|
+
CONSENSUS_CONFIG_SCHEMA_VERSION_BYTE_LENGTH,
|
|
13
|
+
ConsensusVersion,
|
|
14
|
+
CustomEventType,
|
|
15
|
+
EntryType,
|
|
16
|
+
EPOCH_BYTE_LENGTH,
|
|
17
|
+
EventType,
|
|
18
|
+
HASH_BYTE_LENGTH,
|
|
11
19
|
HYPERBEE_KEY_ENCODING,
|
|
12
20
|
HYPERBEE_VALUE_ENCODING,
|
|
13
|
-
|
|
14
|
-
ADMIN_INITIAL_STAKED_BALANCE,
|
|
21
|
+
OperationType,
|
|
15
22
|
TRAC_NAMESPACE,
|
|
16
|
-
|
|
23
|
+
UINT32_MAX,
|
|
24
|
+
VDF_PROOF_BYTE_LENGTHS,
|
|
25
|
+
HTLC_MIN_LOCK_DURATION_EPOCHS,
|
|
26
|
+
PUBLIC_KEY_LENGTH,
|
|
17
27
|
} from '../../utils/constants.js';
|
|
18
|
-
import {
|
|
28
|
+
import {isHexString, isTransactionRecordPut, sleep} from '../../utils/helpers.js';
|
|
19
29
|
import tracCryptoApi from 'trac-crypto-api';
|
|
20
|
-
import
|
|
21
|
-
import
|
|
22
|
-
import {
|
|
30
|
+
import {verifyWesolowski} from '@tracsystems/trac-vdf';
|
|
31
|
+
import StateValidationSchema from './validators/StateValidationSchema.js';
|
|
32
|
+
import {
|
|
33
|
+
safeDecodeApplyOperation,
|
|
34
|
+
safeDecodeConsensusConfig,
|
|
35
|
+
safeDecodeEpochProofV1,
|
|
36
|
+
safeEncodeConsensusConfig,
|
|
37
|
+
safeEncodeEpochProofV1,
|
|
38
|
+
safeEncodeEpochRecord
|
|
39
|
+
} from '../../codecs/apply/applyOperationCodec.js';
|
|
40
|
+
import {
|
|
41
|
+
createMessage,
|
|
42
|
+
deepCopyBuffer,
|
|
43
|
+
incrementBuffer,
|
|
44
|
+
isBufferValid,
|
|
45
|
+
isZeroBuffer,
|
|
46
|
+
NULL_BUFFER,
|
|
47
|
+
safeReadUint32BE,
|
|
48
|
+
safeReadUint8,
|
|
49
|
+
safeToUIntString,
|
|
50
|
+
safeWriteUInt32BE,
|
|
51
|
+
uint16ToBuffer,
|
|
52
|
+
ZERO_WK
|
|
53
|
+
} from '../../utils/buffer.js';
|
|
54
|
+
import {
|
|
55
|
+
safeDecodeProofProposal,
|
|
56
|
+
safeDecodeProofProposalApproval
|
|
57
|
+
} from '../../codecs/consensus/v1/consensusV1OperationCodec.js';
|
|
23
58
|
import addressUtils from './utils/address.js';
|
|
24
59
|
import adminEntryUtils from './utils/adminEntry.js';
|
|
25
|
-
import nodeEntryUtils, { setWritingKey
|
|
60
|
+
import nodeEntryUtils, {NODE_ENTRY_SIZE, setWritingKey} from './utils/nodeEntry.js';
|
|
26
61
|
import nodeRoleUtils from './utils/roles.js';
|
|
27
62
|
import lengthEntryUtils from './utils/lengthEntry.js';
|
|
28
|
-
import transactionUtils from './utils/transaction.js';
|
|
63
|
+
import transactionUtils, {Status} from './utils/transaction.js';
|
|
29
64
|
import {
|
|
30
65
|
BALANCE_FEE,
|
|
31
|
-
toBalance,
|
|
32
|
-
PERCENT_75,
|
|
33
|
-
PERCENT_50,
|
|
34
|
-
PERCENT_25,
|
|
35
66
|
BALANCE_TO_STAKE,
|
|
36
67
|
BALANCE_ZERO,
|
|
68
|
+
PERCENT_25,
|
|
69
|
+
PERCENT_50,
|
|
70
|
+
PERCENT_75,
|
|
71
|
+
toBalance,
|
|
37
72
|
toTerm,
|
|
38
73
|
} from './utils/balance.js';
|
|
39
|
-
import { safeWriteUInt32BE } from '../../utils/buffer.js';
|
|
40
74
|
import deploymentEntryUtils from './utils/deploymentEntry.js';
|
|
41
|
-
import
|
|
42
|
-
import { Status } from './utils/transaction.js';
|
|
75
|
+
import * as escrowEntryUtils from './utils/escrowEntry.js';
|
|
43
76
|
import remote from 'hypercore/lib/fully-remote-proof.js'
|
|
44
77
|
import PQueue from 'p-queue';
|
|
78
|
+
import {createGenesisEpochProof} from './utils/epochProof.js';
|
|
79
|
+
import {decodeVersionedConsensusConfig} from '../../utils/consensusConfig.js';
|
|
80
|
+
import {safeDecodeVdfConfig} from '../../codecs/consensus/v1/vdfConfigCodec.js';
|
|
81
|
+
import { readEpochAppenderSelectionContext, readEpochAppendOutcome } from '../consensus/epochAppenderSelection/epochAppenderSelectionLedgerReader.js';
|
|
82
|
+
import _ from 'lodash';
|
|
83
|
+
import {StateEventQueue} from './StateEventQueue.js';
|
|
84
|
+
import {
|
|
85
|
+
createHtlcLockSigningMessage,
|
|
86
|
+
verifyOrderedHtlcCosignerSignatures,
|
|
87
|
+
} from '../../utils/htlcLock.js';
|
|
88
|
+
import { ConfirmationWatch } from './ConfirmationWatch.js';
|
|
45
89
|
|
|
46
90
|
const OVERSIZED_BATCH_PENALTY_MULTIPLIER = BATCH_SIZE;
|
|
47
91
|
|
|
@@ -55,7 +99,11 @@ class State extends ReadyResource {
|
|
|
55
99
|
#writingKey;
|
|
56
100
|
#config
|
|
57
101
|
#wallet
|
|
102
|
+
#stateValidationSchema;
|
|
58
103
|
#activeWriterCountCache = new Map();
|
|
104
|
+
#stateEventQueue = new StateEventQueue();
|
|
105
|
+
#viewUpdateListener;
|
|
106
|
+
#confirmationWatch = null;
|
|
59
107
|
|
|
60
108
|
/**
|
|
61
109
|
* @param {Corestore} store
|
|
@@ -69,7 +117,8 @@ class State extends ReadyResource {
|
|
|
69
117
|
this.#wallet = wallet
|
|
70
118
|
this.#store = store;
|
|
71
119
|
|
|
72
|
-
this
|
|
120
|
+
this.#stateValidationSchema = new StateValidationSchema(config);
|
|
121
|
+
this.#viewUpdateListener = this.handleViewUpdate.bind(this);
|
|
73
122
|
this.#base = new Autobase(this.#store, this.#config.bootstrap, {
|
|
74
123
|
ackInterval: ACK_INTERVAL,
|
|
75
124
|
valueEncoding: AUTOBASE_VALUE_ENCODING,
|
|
@@ -88,6 +137,10 @@ class State extends ReadyResource {
|
|
|
88
137
|
return this.#writingKey;
|
|
89
138
|
}
|
|
90
139
|
|
|
140
|
+
get stateValidationSchema() {
|
|
141
|
+
return this.#stateValidationSchema;
|
|
142
|
+
}
|
|
143
|
+
|
|
91
144
|
get applyHandler() {
|
|
92
145
|
return this.#apply.bind(this);
|
|
93
146
|
}
|
|
@@ -96,20 +149,67 @@ class State extends ReadyResource {
|
|
|
96
149
|
console.log("State initialization...")
|
|
97
150
|
await this.#base.ready();
|
|
98
151
|
this.#writingKey = this.#base.local.key;
|
|
152
|
+
|
|
153
|
+
// Catch-up events describe state which is already available at startup.
|
|
154
|
+
this.#stateEventQueue.clear();
|
|
155
|
+
this.#base.on('update', this.#viewUpdateListener);
|
|
156
|
+
await this.#listeners()
|
|
157
|
+
this.#confirmationWatch = new ConfirmationWatch(this.#base, { timeout: this.#config.confirmationStallTimeout });
|
|
158
|
+
this.#confirmationWatch.start();
|
|
99
159
|
}
|
|
100
160
|
|
|
101
161
|
async _close() {
|
|
102
162
|
console.log("State: closing gracefully...");
|
|
163
|
+
this.#confirmationWatch?.stop();
|
|
164
|
+
|
|
165
|
+
this.#base.off('update', this.#viewUpdateListener);
|
|
166
|
+
this.#stateEventQueue.clear();
|
|
167
|
+
this.removeAllListeners();
|
|
168
|
+
|
|
103
169
|
if (this.#bee !== null) {
|
|
104
170
|
await this.#bee.close();
|
|
105
171
|
}
|
|
106
172
|
await sleep(100);
|
|
107
173
|
|
|
174
|
+
[EventType.IS_INDEXER,
|
|
175
|
+
EventType.IS_NON_INDEXER,
|
|
176
|
+
EventType.WRITABLE,
|
|
177
|
+
EventType.UNWRITABLE
|
|
178
|
+
].forEach(event => {
|
|
179
|
+
this.#base.removeAllListeners(event)
|
|
180
|
+
})
|
|
181
|
+
|
|
108
182
|
if (this.#base !== null) {
|
|
109
183
|
await this.#base.close();
|
|
110
184
|
}
|
|
111
185
|
await sleep(100);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
async #listeners() {
|
|
190
|
+
this.#base.on(EventType.IS_INDEXER, () => {
|
|
191
|
+
console.log("Current node is an indexer");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
this.#base.on(EventType.IS_NON_INDEXER, async () => {
|
|
195
|
+
// Prevent further actions if closing is in progress
|
|
196
|
+
// The reason is that getNodeEntry is async and may cause issues if we will access state after closing
|
|
197
|
+
console.log("Current node is not an indexer anymore");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
this.#base.on(EventType.WRITABLE, async () => {
|
|
201
|
+
console.log("Current node is writable");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
this.#base.on(EventType.UNWRITABLE, async () => {
|
|
205
|
+
console.log("Current node is unwritable");
|
|
206
|
+
});
|
|
207
|
+
}
|
|
112
208
|
|
|
209
|
+
#emitEvent(event, ...args) {
|
|
210
|
+
try {
|
|
211
|
+
this.emit(event, ...args)
|
|
212
|
+
} catch (_ignored) { }
|
|
113
213
|
}
|
|
114
214
|
|
|
115
215
|
isWritable() {
|
|
@@ -120,6 +220,10 @@ class State extends ReadyResource {
|
|
|
120
220
|
return this.#base.isIndexer;
|
|
121
221
|
}
|
|
122
222
|
|
|
223
|
+
async indexerCount() {
|
|
224
|
+
return (await this.getIndexersEntry()).length;
|
|
225
|
+
}
|
|
226
|
+
|
|
123
227
|
getUnsignedLength() {
|
|
124
228
|
return this.#base.view.core.length;
|
|
125
229
|
}
|
|
@@ -128,6 +232,110 @@ class State extends ReadyResource {
|
|
|
128
232
|
return this.#base.view.core.signedLength;
|
|
129
233
|
}
|
|
130
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Reads the current epoch id from signed state.
|
|
237
|
+
*
|
|
238
|
+
* @returns {Promise<bigint|null>} Current epoch id, or `null` when genesis
|
|
239
|
+
* epoch state has not been initialized.
|
|
240
|
+
*/
|
|
241
|
+
async getCurrentEpoch() {
|
|
242
|
+
const currentEpoch = await this.getSigned(EntryType.EPOCH_CURRENT);
|
|
243
|
+
return _.isNil(currentEpoch) ? null : currentEpoch.readBigUInt64BE(0);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async getCurrentEpochUnsigned() {
|
|
247
|
+
const currentEpoch = await this.get(EntryType.EPOCH_CURRENT);
|
|
248
|
+
return isBufferValid(currentEpoch, EPOCH_BYTE_LENGTH) ? currentEpoch.readBigUInt64BE(0) : null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Reads the required current epoch id from signed state.
|
|
253
|
+
*
|
|
254
|
+
* @returns {Promise<bigint>} Current epoch id.
|
|
255
|
+
* @throws {Error} When genesis epoch state has not been initialized.
|
|
256
|
+
*/
|
|
257
|
+
async requireCurrentEpoch() {
|
|
258
|
+
const currentEpoch = await this.getCurrentEpoch();
|
|
259
|
+
if (_.isNil(currentEpoch)) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
'Current epoch is not initialized. Genesis epoch has not been set.'
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return currentEpoch;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Reads the epoch hash stored under `/epoch/<epoch>`.
|
|
269
|
+
*
|
|
270
|
+
* @param {bigint|number|string} count Epoch id.
|
|
271
|
+
* @returns {Promise<Buffer|null>} Epoch hash, or `null` when the epoch is not stored.
|
|
272
|
+
* @throws {Error} When epoch id is missing or invalid.
|
|
273
|
+
*/
|
|
274
|
+
async getEpoch(count) {
|
|
275
|
+
if (_.isNil(count)) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
'Cannot read epoch: epoch id is required.'
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const epochId = typeof count === 'bigint' ? count : BigInt(count);
|
|
282
|
+
return await this.getSigned(EntryType.EPOCH + epochId.toString());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Reads the required epoch hash stored under `/epoch/<epoch>`.
|
|
287
|
+
*
|
|
288
|
+
* @param {bigint|number|string} count Epoch id.
|
|
289
|
+
* @returns {Promise<Buffer>} Epoch hash.
|
|
290
|
+
* @throws {Error} When epch id is missing or invalid, or the epoch is not stored.
|
|
291
|
+
*/
|
|
292
|
+
async requireEpoch(count) {
|
|
293
|
+
const epochHash = await this.getEpoch(count);
|
|
294
|
+
if (_.isNil(epochHash)) {
|
|
295
|
+
const epochId = typeof count === 'bigint' ? count : BigInt(count);
|
|
296
|
+
throw new Error(
|
|
297
|
+
`Cannot read epoch ${epochId}: epoch is not initialized or does not exist.`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return epochHash;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Reads the encoded epoch record stored under `/epochHash/<epochHash>`.
|
|
305
|
+
* Decode its { sv, data } envelope with decodeEpochRecord, then decode data
|
|
306
|
+
* according to the stored sv, not the currently active consensus version.
|
|
307
|
+
*
|
|
308
|
+
* @param {Buffer|string} epochHash Epoch hash as a buffer or hex string.
|
|
309
|
+
* @returns {Promise<Buffer|null>} Encoded epoch record, or `null` when it is not stored.
|
|
310
|
+
* @throws {Error} When epoch hash is missing.
|
|
311
|
+
*/
|
|
312
|
+
async getEpochProof(epochHash) {
|
|
313
|
+
if (_.isNil(epochHash)) {
|
|
314
|
+
throw new Error('Cannot read epoch proof: epoch hash is required.');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const epochHashString = b4a.isBuffer(epochHash) ? epochHash.toString('hex') : epochHash.toString();
|
|
318
|
+
return await this.getSigned(EntryType.EPOCH_HASH + epochHashString);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Reads the required encoded { sv, data } epoch record under `/epochHash/<epochHash>`.
|
|
323
|
+
*
|
|
324
|
+
* @param {Buffer|string} epochHash Epoch hash as a buffer or hex string.
|
|
325
|
+
* @returns {Promise<Buffer>} Encoded epoch record.
|
|
326
|
+
* @throws {Error} When epoch hash is missing or the epoch proof is not stored.
|
|
327
|
+
*/
|
|
328
|
+
async requireEpochProof(epochHash) {
|
|
329
|
+
const epochProof = await this.getEpochProof(epochHash);
|
|
330
|
+
if (_.isNil(epochProof)) {
|
|
331
|
+
const epochHashString = b4a.isBuffer(epochHash) ? epochHash.toString('hex') : epochHash.toString();
|
|
332
|
+
throw new Error(
|
|
333
|
+
`Cannot read epoch proof ${epochHashString}: epoch proof is not initialized or does not exist.`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return epochProof;
|
|
337
|
+
}
|
|
338
|
+
|
|
131
339
|
getFee() {
|
|
132
340
|
return transactionUtils.FEE;
|
|
133
341
|
}
|
|
@@ -149,10 +357,22 @@ class State extends ReadyResource {
|
|
|
149
357
|
return false;
|
|
150
358
|
}
|
|
151
359
|
|
|
152
|
-
|
|
360
|
+
/**
|
|
361
|
+
* Reads a value from the view checked out at the current signed length.
|
|
362
|
+
* Consensus channel admission uses { wait: false, update: false } to avoid
|
|
363
|
+
* waiting for replication while deciding whether to open the channel.
|
|
364
|
+
*
|
|
365
|
+
* @param {string|Buffer} key Entry key.
|
|
366
|
+
* @param {object} [options={}] Read options forwarded to Hyperbee.
|
|
367
|
+
* @param {boolean} [options.wait=true] Wait for missing blocks to be replicated.
|
|
368
|
+
* When false, a missing block causes an error rather than waiting.
|
|
369
|
+
* @param {boolean} [options.update=true] Allow Hyperbee to update an empty view before reading.
|
|
370
|
+
* @returns {Promise<Buffer|null>} Stored value, or null if the key does not exist.
|
|
371
|
+
*/
|
|
372
|
+
async getSigned(key, options = {}) {
|
|
153
373
|
const view_session = this.#base.view.checkout(this.#base.view.core.signedLength);
|
|
154
374
|
try {
|
|
155
|
-
const result = await view_session.get(key);
|
|
375
|
+
const result = await view_session.get(key, options);
|
|
156
376
|
return result ? result.value : null;
|
|
157
377
|
} finally {
|
|
158
378
|
await view_session.close();
|
|
@@ -192,6 +412,11 @@ class State extends ReadyResource {
|
|
|
192
412
|
return !!adminEntry && this.#wallet?.address === adminEntry?.address && b4a.equals(adminEntry?.wk, this.writingKey)
|
|
193
413
|
}
|
|
194
414
|
|
|
415
|
+
async isAdminAddress(targetAddress) {
|
|
416
|
+
const adminEntry = await this.getAdminEntry();
|
|
417
|
+
return (adminEntry?.address === targetAddress);
|
|
418
|
+
}
|
|
419
|
+
|
|
195
420
|
async isAdminAllowedToValidate() {
|
|
196
421
|
if (!this.writingKey) return false;
|
|
197
422
|
|
|
@@ -212,6 +437,23 @@ class State extends ReadyResource {
|
|
|
212
437
|
return Object.values(this.#base.system.indexers);
|
|
213
438
|
}
|
|
214
439
|
|
|
440
|
+
/**
|
|
441
|
+
* Checks whether a bech32m address belongs to a registered indexer.
|
|
442
|
+
* @param {string} targetAddress Address to check.
|
|
443
|
+
* @param {object} [options] Hyperbee read options, e.g. `wait: false` for channel admission.
|
|
444
|
+
* @returns {Promise<boolean>} True when the address belongs to an indexer in signed state.
|
|
445
|
+
*/
|
|
446
|
+
async isIndexerAddress(targetAddress, options) {
|
|
447
|
+
const targetAddressBuffer = addressUtils.addressToBuffer(targetAddress, this.#config.addressPrefix);
|
|
448
|
+
if (targetAddressBuffer.length === 0) return false;
|
|
449
|
+
const entries = await this.getIndexersEntry();
|
|
450
|
+
for (const entry of entries) {
|
|
451
|
+
const address = await this.getSigned(EntryType.WRITER_ADDRESS + b4a.toString(entry.key, 'hex'), options);
|
|
452
|
+
if (address && b4a.equals(targetAddressBuffer, address)) return true;
|
|
453
|
+
}
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
|
|
215
457
|
async getActiveWriterCount(excludeAdmin = false) {
|
|
216
458
|
const cached = this.#activeWriterCountCache.get(excludeAdmin);
|
|
217
459
|
const systemLength = this.#base.system?.core?.length ?? -1;
|
|
@@ -223,6 +465,10 @@ class State extends ReadyResource {
|
|
|
223
465
|
const activeAddresses = new Set();
|
|
224
466
|
const adminAddress = (await this.getAdminEntry())?.address ?? null;
|
|
225
467
|
|
|
468
|
+
// sometimes autobase swaps _applyState while performing a bump which will generate the system to be "empty" (it is just a proxy getter to applyState with a null check).
|
|
469
|
+
// since this thing is responsible for permissions, it was opted out to perform a realignment check prior instead of safe navigating to respond the query (especially given that there is a cache in place).
|
|
470
|
+
await this.refresh()
|
|
471
|
+
|
|
226
472
|
for await (const { key, value } of this.#base.system.list()) {
|
|
227
473
|
if (!key || !value || value.isRemoved) continue;
|
|
228
474
|
|
|
@@ -245,11 +491,26 @@ class State extends ReadyResource {
|
|
|
245
491
|
return count;
|
|
246
492
|
}
|
|
247
493
|
|
|
494
|
+
async refresh() {
|
|
495
|
+
return await this.#base.update()
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Publishes apply events after the related state changes become signed.
|
|
500
|
+
* The optional length is used by tests which provide their own Autobase instance.
|
|
501
|
+
*
|
|
502
|
+
* @param {number} signedLength current signed view length
|
|
503
|
+
*/
|
|
504
|
+
handleViewUpdate(signedLength = this.getSignedLength()) {
|
|
505
|
+
this.#stateEventQueue.flush(signedLength, (event, ...args) => {
|
|
506
|
+
this.#emitEvent(event, ...args);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
248
510
|
async isWkInIndexersEntry(wk) {
|
|
249
511
|
if (wk === null) return false;
|
|
250
|
-
|
|
512
|
+
return Object.values(this.#base.system.indexers)
|
|
251
513
|
.some(entry => b4a.equals(entry.key, wk));
|
|
252
|
-
return indexerListHasWk;
|
|
253
514
|
}
|
|
254
515
|
|
|
255
516
|
async getWriterLength() {
|
|
@@ -363,7 +624,7 @@ class State extends ReadyResource {
|
|
|
363
624
|
if (initialization === null) {
|
|
364
625
|
return false
|
|
365
626
|
} else {
|
|
366
|
-
return b4a.equals(initialization, safeWriteUInt32BE(0
|
|
627
|
+
return b4a.equals(initialization, safeWriteUInt32BE(0))
|
|
367
628
|
}
|
|
368
629
|
}
|
|
369
630
|
|
|
@@ -390,138 +651,822 @@ class State extends ReadyResource {
|
|
|
390
651
|
throw new Error("Params must be non-negative");
|
|
391
652
|
}
|
|
392
653
|
|
|
393
|
-
if (startSignedLength > endSignedLength) {
|
|
394
|
-
throw new Error("endSignedLength must be greater than or equal to startSignedLength");
|
|
654
|
+
if (startSignedLength > endSignedLength) {
|
|
655
|
+
throw new Error("endSignedLength must be greater than or equal to startSignedLength");
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (startSignedLength === endSignedLength) return [];
|
|
659
|
+
|
|
660
|
+
const currentSignedLength = this.getSignedLength();
|
|
661
|
+
const signedLength2End = Math.min(currentSignedLength, endSignedLength);
|
|
662
|
+
|
|
663
|
+
const startSeq = startSignedLength;
|
|
664
|
+
const endSeq = signedLength2End - 1;
|
|
665
|
+
|
|
666
|
+
if (startSeq > endSeq) {
|
|
667
|
+
throw new Error("Invalid range");
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const historyStream = this.#base.view.createHistoryStream({
|
|
671
|
+
gte: startSeq,
|
|
672
|
+
lte: endSeq
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
let hashes = [];
|
|
676
|
+
for await (const entry of historyStream) {
|
|
677
|
+
if (isTransactionRecordPut(entry)) {
|
|
678
|
+
hashes.push({ hash: entry.key, confirmed_length: entry.seq });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return hashes;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async getRegisteredWriterKey(writingKey) {
|
|
686
|
+
const entry = await this.get(EntryType.WRITER_ADDRESS + writingKey);
|
|
687
|
+
return entry ? addressUtils.addressToBuffer(entry, this.#config.addressPrefix) : null;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
#setupHyperbee(store) {
|
|
691
|
+
this.#bee = new Hyperbee(store.get(TRAC_NAMESPACE), {
|
|
692
|
+
extension: false,
|
|
693
|
+
keyEncoding: HYPERBEE_KEY_ENCODING,
|
|
694
|
+
valueEncoding: HYPERBEE_VALUE_ENCODING,
|
|
695
|
+
})
|
|
696
|
+
return this.#bee;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async getSignedConsensusConfig() {
|
|
700
|
+
const currentConfigPointer = await this.getSigned(EntryType.CONSENSUS_CONFIG_CURRENT);
|
|
701
|
+
if (_.isNil(currentConfigPointer)) return null;
|
|
702
|
+
|
|
703
|
+
if (!b4a.isBuffer(currentConfigPointer) || currentConfigPointer.length !== 4) {
|
|
704
|
+
throw new Error('Invalid current consensus config pointer.');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const currentConfigIndex = currentConfigPointer.readUInt32BE(0);
|
|
708
|
+
|
|
709
|
+
const encodedConsensusConfig = await this.getSigned(EntryType.CONSENSUS_CONFIG_RECORD + currentConfigIndex);
|
|
710
|
+
if (_.isNil(encodedConsensusConfig)) {
|
|
711
|
+
throw new Error(`Consensus config record ${currentConfigIndex} does not exist.`);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return decodeVersionedConsensusConfig(encodedConsensusConfig);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async requireSignedConsensusConfig() {
|
|
718
|
+
const consensusConfig = await this.getSignedConsensusConfig();
|
|
719
|
+
if (_.isNil(consensusConfig)) {
|
|
720
|
+
throw new Error('Consensus config is not initialized.');
|
|
721
|
+
}
|
|
722
|
+
return consensusConfig;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** @returns {Promise<import('../consensus/epochAppenderSelection/epochAppenderSelectionState.js').EpochAppenderSelectionContext>} Signed arbitration context. */
|
|
726
|
+
getEpochAppenderSelectionContext() {
|
|
727
|
+
return readEpochAppenderSelectionContext(this.#base, this.#config);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* @param {import('../consensus/epochAppenderSelection/epochAppenderSelectionState.js').EpochAppendPublication} publication
|
|
732
|
+
* @returns {Promise<import('../consensus/epochAppenderSelection/epochAppenderSelectionState.js').EpochAppendOutcomeResult>}
|
|
733
|
+
*/
|
|
734
|
+
getEpochAppendOutcome(publication) {
|
|
735
|
+
return readEpochAppendOutcome(this.#base, publication);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** Authorizes the epoch append inside the writer queue before writing to Autobase. */
|
|
739
|
+
appendEpoch(payload, authorizeEpochAppend) {
|
|
740
|
+
return this.#writeQueue.add(async () => {
|
|
741
|
+
await authorizeEpochAppend({
|
|
742
|
+
writerKey: this.#base.local.key.toString('hex'),
|
|
743
|
+
from: this.#base.local.length,
|
|
744
|
+
});
|
|
745
|
+
return this.#base.append(payload);
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// ATTENTION: DO NOT USE METHODS ABOVE IN APPLY PART!
|
|
750
|
+
///////////////////////////////APPLY////////////////////////////////////
|
|
751
|
+
|
|
752
|
+
async #apply(nodes, view, base) {
|
|
753
|
+
const batch = view.batch();
|
|
754
|
+
const batchInvoker = nodes[0].from.key;
|
|
755
|
+
const postApplyEvents = [];
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
if (nodes.length > BATCH_SIZE) {
|
|
759
|
+
await this.#validatorPenaltyApply(batchInvoker, batch, base, OVERSIZED_BATCH_PENALTY_MULTIPLIER);
|
|
760
|
+
await batch.flush();
|
|
761
|
+
await batch.close();
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
let invalidOperations = 0;
|
|
766
|
+
|
|
767
|
+
for (const node of nodes) {
|
|
768
|
+
|
|
769
|
+
if (b4a.byteLength(node.value) > transactionUtils.MAXIMUM_OPERATION_PAYLOAD_SIZE) {
|
|
770
|
+
this.#safeLogApply("Node payload exceeds the maximum operation payload size.", node.from.key)
|
|
771
|
+
invalidOperations++;
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const op = safeDecodeApplyOperation(node.value);
|
|
776
|
+
|
|
777
|
+
if (!op) {
|
|
778
|
+
this.#safeLogApply("Failed to decode operation.", node.from.key)
|
|
779
|
+
invalidOperations++;
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const handler = this.#getApplyOperationHandler(op.type);
|
|
784
|
+
|
|
785
|
+
if (handler) {
|
|
786
|
+
const result = await handler(op, view, base, node, batch, postApplyEvents);
|
|
787
|
+
if (result === Status.FAILURE) {
|
|
788
|
+
invalidOperations++;
|
|
789
|
+
} else if (result === Status.IGNORE) {
|
|
790
|
+
continue;
|
|
791
|
+
} else if (result !== Status.SUCCESS) {
|
|
792
|
+
this.#safeLogApply(`Unknown operation status: ${result}`, node.from.key);
|
|
793
|
+
invalidOperations++;
|
|
794
|
+
}
|
|
795
|
+
} else {
|
|
796
|
+
this.#safeLogApply(`Unknown operation type: ${op.type}`, node.from.key)
|
|
797
|
+
invalidOperations++;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (invalidOperations > 0) {
|
|
801
|
+
await this.#validatorPenaltyApply(batchInvoker, batch, base, invalidOperations);
|
|
802
|
+
this.#safeLogApply(`Applied with ${invalidOperations} invalid operations.`)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
await batch.flush();
|
|
806
|
+
await batch.close();
|
|
807
|
+
|
|
808
|
+
if (postApplyEvents.length !== 0) {
|
|
809
|
+
try {
|
|
810
|
+
this.#stateEventQueue.enqueue(postApplyEvents, view.core.length);
|
|
811
|
+
} catch (error) {
|
|
812
|
+
this.#safeLogApply('StateEventQueue', `Failed to queue post apply events: ${String(error)}`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
#getApplyOperationHandler(type) {
|
|
818
|
+
const handlers = {
|
|
819
|
+
[OperationType.BALANCE_INITIALIZATION]: this.#handleApplyInitializeBalanceOperation.bind(this),
|
|
820
|
+
[OperationType.DISABLE_INITIALIZATION]: this.#handleApplyDisableBalanceInitializationOperation.bind(this),
|
|
821
|
+
[OperationType.ADD_ADMIN]: this.#handleApplyAddAdminOperation.bind(this),
|
|
822
|
+
[OperationType.APPEND_WHITELIST]: this.#handleApplyAppendWhitelistOperation.bind(this),
|
|
823
|
+
[OperationType.ADD_WRITER]: this.#handleApplyAddWriterOperation.bind(this),
|
|
824
|
+
[OperationType.REMOVE_WRITER]: this.#handleApplyRemoveWriterOperation.bind(this),
|
|
825
|
+
[OperationType.ADMIN_RECOVERY]: this.#handleApplyAdminRecoveryOperation.bind(this),
|
|
826
|
+
[OperationType.ADD_INDEXER]: this.#handleApplyAddIndexerOperation.bind(this),
|
|
827
|
+
[OperationType.REMOVE_INDEXER]: this.#handleApplyRemoveIndexerOperation.bind(this),
|
|
828
|
+
[OperationType.BAN_VALIDATOR]: this.#handleApplyBanValidatorOperation.bind(this),
|
|
829
|
+
[OperationType.BOOTSTRAP_DEPLOYMENT]: this.#handleApplyBootstrapDeploymentOperation.bind(this),
|
|
830
|
+
[OperationType.TX]: this.#handleApplyTxOperation.bind(this),
|
|
831
|
+
[OperationType.TRANSFER]: this.#handleApplyTransferOperation.bind(this),
|
|
832
|
+
[OperationType.SET_EPOCH]: this.#handleApplySetEpochOperation.bind(this),
|
|
833
|
+
[OperationType.SET_GENESIS_EPOCH]: this.#handleApplySetGenesisEpoch.bind(this),
|
|
834
|
+
[OperationType.SET_CONSENSUS_CONFIG]: this.#handleApplySetConsensusConfig.bind(this),
|
|
835
|
+
[OperationType.HTLC_LOCK]: this.#handleApplyHtlcLockOperation.bind(this),
|
|
836
|
+
[OperationType.HTLC_CLAIM]: this.#handleApplyHtlcClaimOperation.bind(this),
|
|
837
|
+
[OperationType.HTLC_REFUND]: this.#handleApplyHtlcRefundOperation.bind(this),
|
|
838
|
+
};
|
|
839
|
+
return handlers[type] || null;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async #handleApplyHtlcLockOperation(op, view, base, node, batch) {
|
|
843
|
+
if (!this.#stateValidationSchema.validateHtlcLockOperation(op)) {
|
|
844
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Contract schema validation failed.", node.from.key);
|
|
845
|
+
return Status.FAILURE;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (!Object.hasOwn(op.hlo, "vs") || !Object.hasOwn(op.hlo, "va") || !Object.hasOwn(op.hlo, "vn")) {
|
|
849
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Operation is not complete.", node.from.key);
|
|
850
|
+
return Status.FAILURE;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (b4a.equals(op.address, op.hlo.va)) {
|
|
854
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Validator cannot sign its own transaction.", node.from.key);
|
|
855
|
+
return Status.FAILURE;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (b4a.equals(op.hlo.in, op.hlo.vn)) {
|
|
859
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Nonces should not be the same.", node.from.key);
|
|
860
|
+
return Status.FAILURE;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (b4a.equals(op.hlo.is, op.hlo.vs)) {
|
|
864
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Signatures should not be the same.", node.from.key);
|
|
865
|
+
return Status.FAILURE;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const lockerAddressString = addressUtils.bufferToAddress(op.address, this.#config.addressPrefix);
|
|
869
|
+
if (lockerAddressString === null) {
|
|
870
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Locker address is invalid.", node.from.key);
|
|
871
|
+
return Status.FAILURE;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const lockerPublicKey = tracCryptoApi.address.decodeSafe(lockerAddressString);
|
|
875
|
+
if (
|
|
876
|
+
!b4a.isBuffer(lockerPublicKey) ||
|
|
877
|
+
lockerPublicKey.length !== PUBLIC_KEY_LENGTH ||
|
|
878
|
+
isZeroBuffer(lockerPublicKey)
|
|
879
|
+
) {
|
|
880
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Failed to decode locker public key.", node.from.key);
|
|
881
|
+
return Status.FAILURE;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
let lockerMessage;
|
|
885
|
+
try {
|
|
886
|
+
lockerMessage = createHtlcLockSigningMessage(
|
|
887
|
+
this.#config.networkId,
|
|
888
|
+
op.address,
|
|
889
|
+
op.hlo
|
|
890
|
+
);
|
|
891
|
+
} catch {
|
|
892
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Invalid locker message.", node.from.key);
|
|
893
|
+
return Status.FAILURE;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const regeneratedTxHash = await tracCryptoApi.hash.blake3Safe(lockerMessage);
|
|
897
|
+
if (!isBufferValid(regeneratedTxHash, HASH_BYTE_LENGTH)) {
|
|
898
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Failed to hash locker message.", node.from.key);
|
|
899
|
+
return Status.FAILURE;
|
|
900
|
+
}
|
|
901
|
+
if (!b4a.equals(regeneratedTxHash, op.hlo.tx)) {
|
|
902
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Message hash does not match the tx_hash.", node.from.key);
|
|
903
|
+
return Status.FAILURE;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (!tracCryptoApi.signature.verify(op.hlo.is, regeneratedTxHash, lockerPublicKey)) {
|
|
907
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Failed to verify locker signature.", node.from.key);
|
|
908
|
+
return Status.FAILURE;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (!b4a.equals(lockerPublicKey, op.hlo.ss[0])) {
|
|
912
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Signer zero does not match the locker.", node.from.key);
|
|
913
|
+
return Status.FAILURE;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (
|
|
917
|
+
1 + op.hlo.cs.length < op.hlo.th[0] ||
|
|
918
|
+
!verifyOrderedHtlcCosignerSignatures(op.hlo.cs, op.hlo.tx, op.hlo.ss)
|
|
919
|
+
) {
|
|
920
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "HTLC cosigner authorization is invalid.", node.from.key);
|
|
921
|
+
return Status.FAILURE;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const validatorAddressString = addressUtils.bufferToAddress(op.hlo.va, this.#config.addressPrefix);
|
|
925
|
+
if (validatorAddressString === null) {
|
|
926
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Validator address is invalid.", node.from.key);
|
|
927
|
+
return Status.FAILURE;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const validatorPublicKey = tracCryptoApi.address.decodeSafe(validatorAddressString);
|
|
931
|
+
if (
|
|
932
|
+
!b4a.isBuffer(validatorPublicKey) ||
|
|
933
|
+
validatorPublicKey.length !== PUBLIC_KEY_LENGTH ||
|
|
934
|
+
isZeroBuffer(validatorPublicKey)
|
|
935
|
+
) {
|
|
936
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Failed to decode validator public key.", node.from.key);
|
|
937
|
+
return Status.FAILURE;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const validatorMessage = createMessage(
|
|
941
|
+
this.#config.networkId,
|
|
942
|
+
op.hlo.tx,
|
|
943
|
+
op.hlo.vn,
|
|
944
|
+
OperationType.HTLC_LOCK
|
|
945
|
+
);
|
|
946
|
+
if (validatorMessage.length === 0) {
|
|
947
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Invalid validator message.", node.from.key);
|
|
948
|
+
return Status.FAILURE;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const validatorMessageHash = await tracCryptoApi.hash.blake3Safe(validatorMessage);
|
|
952
|
+
if (!isBufferValid(validatorMessageHash, HASH_BYTE_LENGTH)) {
|
|
953
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Failed to hash validator message.", node.from.key);
|
|
954
|
+
return Status.FAILURE;
|
|
955
|
+
}
|
|
956
|
+
if (!tracCryptoApi.signature.verify(op.hlo.vs, validatorMessageHash, validatorPublicKey)) {
|
|
957
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Failed to verify validator signature.", node.from.key);
|
|
958
|
+
return Status.FAILURE;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const indexersSequenceState = await this.#getIndexerSequenceStateApply(base);
|
|
962
|
+
if (indexersSequenceState === null) {
|
|
963
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Indexer sequence state is invalid.", node.from.key);
|
|
964
|
+
return Status.FAILURE;
|
|
965
|
+
}
|
|
966
|
+
if (!b4a.equals(op.hlo.txv, indexersSequenceState)) {
|
|
967
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Transaction was not executed.", node.from.key);
|
|
968
|
+
return Status.FAILURE;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const validatorEntryBuffer = await this.#getEntryApply(validatorAddressString, batch);
|
|
972
|
+
if (!await this.#isValidatorValidApply(validatorEntryBuffer, node, op)) {
|
|
973
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Validator consistency check failed.", node.from.key);
|
|
974
|
+
return Status.FAILURE;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const lockIdHex = b4a.toString(op.hlo.tx, 'hex');
|
|
978
|
+
const operationEntry = await this.#getEntryApply(lockIdHex, batch);
|
|
979
|
+
if (operationEntry !== null) {
|
|
980
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Operation has already been applied.", node.from.key);
|
|
981
|
+
return Status.IGNORE;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const escrowKey = EntryType.HTLC_ESCROW + lockIdHex;
|
|
985
|
+
const existingEscrow = await this.#getEntryApply(escrowKey, batch);
|
|
986
|
+
if (existingEscrow !== null) {
|
|
987
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "HTLC escrow already exists.", node.from.key);
|
|
988
|
+
return Status.FAILURE;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const currentEpochBuffer = await this.#getEntryApply(EntryType.EPOCH_CURRENT, batch);
|
|
992
|
+
if (!isBufferValid(currentEpochBuffer, EPOCH_BYTE_LENGTH)) {
|
|
993
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Current epoch is not initialized or is invalid.", node.from.key);
|
|
994
|
+
return Status.FAILURE;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const currentEpoch = currentEpochBuffer.readBigUInt64BE(0);
|
|
998
|
+
const refundEpoch = op.hlo.re.readBigUInt64BE(0);
|
|
999
|
+
if (refundEpoch < currentEpoch + HTLC_MIN_LOCK_DURATION_EPOCHS) {
|
|
1000
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Refund epoch is below the minimum lock duration.", node.from.key);
|
|
1001
|
+
return Status.IGNORE;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const principal = toBalance(op.hlo.am);
|
|
1005
|
+
const surchargeFee = toBalance(op.hlo.fa);
|
|
1006
|
+
const networkFee = toBalance(transactionUtils.FEE);
|
|
1007
|
+
if (principal === null || surchargeFee === null || networkFee === null) {
|
|
1008
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Invalid HTLC or network fee amount.", node.from.key);
|
|
1009
|
+
return Status.FAILURE;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const escrowAmount = principal.add(surchargeFee);
|
|
1013
|
+
const totalDeductedAmount = escrowAmount?.add(networkFee) ?? null;
|
|
1014
|
+
if (escrowAmount === null || totalDeductedAmount === null) {
|
|
1015
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "HTLC total amount overflow.", node.from.key);
|
|
1016
|
+
return Status.FAILURE;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const lockerEntryBuffer = await this.#getEntryApply(lockerAddressString, batch);
|
|
1020
|
+
const lockerEntry = nodeEntryUtils.decode(lockerEntryBuffer);
|
|
1021
|
+
if (lockerEntry === null) {
|
|
1022
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Locker account does not exist or is invalid.", node.from.key);
|
|
1023
|
+
return Status.FAILURE;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const lockerBalance = toBalance(lockerEntry.balance);
|
|
1027
|
+
if (lockerBalance === null) {
|
|
1028
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Locker balance is invalid.", node.from.key);
|
|
1029
|
+
return Status.FAILURE;
|
|
1030
|
+
}
|
|
1031
|
+
if (!lockerBalance.greaterThanOrEquals(totalDeductedAmount)) {
|
|
1032
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Insufficient locker balance.", node.from.key);
|
|
1033
|
+
return Status.IGNORE;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const updatedLockerBalance = lockerBalance.sub(totalDeductedAmount);
|
|
1037
|
+
const updatedLockerEntry = updatedLockerBalance?.update(lockerEntryBuffer) ?? null;
|
|
1038
|
+
if (updatedLockerEntry === null) {
|
|
1039
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Failed to update locker balance.", node.from.key);
|
|
1040
|
+
return Status.FAILURE;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const validatorEntry = nodeEntryUtils.decode(validatorEntryBuffer);
|
|
1044
|
+
const validatorBalance = validatorEntry ? toBalance(validatorEntry.balance) : null;
|
|
1045
|
+
const validatorReward = networkFee.percentage(PERCENT_75);
|
|
1046
|
+
if (validatorBalance === null || validatorReward === null) {
|
|
1047
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Validator balance or reward is invalid.", node.from.key);
|
|
1048
|
+
return Status.FAILURE;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const updatedValidatorBalance = validatorBalance.add(validatorReward);
|
|
1052
|
+
const updatedValidatorEntry = updatedValidatorBalance?.update(validatorEntryBuffer) ?? null;
|
|
1053
|
+
if (updatedValidatorEntry === null) {
|
|
1054
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Failed to update validator balance.", node.from.key);
|
|
1055
|
+
return Status.FAILURE;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const escrowEntry = escrowEntryUtils.init({
|
|
1059
|
+
lockId: op.hlo.tx,
|
|
1060
|
+
lockerAddress: op.address,
|
|
1061
|
+
claimRecipientAddress: op.hlo.ca,
|
|
1062
|
+
refundRecipientAddress: op.hlo.ra,
|
|
1063
|
+
amount: op.hlo.am,
|
|
1064
|
+
additionalFeeAmount: op.hlo.fa,
|
|
1065
|
+
additionalFeeRecipientAddress: op.hlo.fr,
|
|
1066
|
+
hashLock: op.hlo.hl,
|
|
1067
|
+
refundEpoch: op.hlo.re,
|
|
1068
|
+
metadata: op.hlo.md,
|
|
1069
|
+
}, this.#config.addressPrefix);
|
|
1070
|
+
if (escrowEntry.length === 0) {
|
|
1071
|
+
this.#safeLogApply(OperationType.HTLC_LOCK, "Failed to encode HTLC escrow.", node.from.key);
|
|
1072
|
+
return Status.FAILURE;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
await batch.put(lockerAddressString, updatedLockerEntry);
|
|
1076
|
+
await batch.put(validatorAddressString, updatedValidatorEntry);
|
|
1077
|
+
await batch.put(escrowKey, escrowEntry);
|
|
1078
|
+
await batch.put(lockIdHex, node.value);
|
|
1079
|
+
|
|
1080
|
+
if (this.#config.enableTxApplyLogs) {
|
|
1081
|
+
console.info(`HTLC lock operation: ${lockIdHex} has been appended.`);
|
|
1082
|
+
}
|
|
1083
|
+
return Status.SUCCESS;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
async #handleApplyHtlcClaimOperation(op, view, base, node, batch) {
|
|
1087
|
+
if (!this.#stateValidationSchema.validateHtlcClaimOperation(op)) {
|
|
1088
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Contract schema validation failed.", node.from.key);
|
|
1089
|
+
return Status.FAILURE;
|
|
1090
|
+
}
|
|
1091
|
+
if (!Object.hasOwn(op.hco, "vs") || !Object.hasOwn(op.hco, "va") || !Object.hasOwn(op.hco, "vn")) {
|
|
1092
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Operation is not complete.", node.from.key);
|
|
1093
|
+
return Status.FAILURE;
|
|
1094
|
+
}
|
|
1095
|
+
if (b4a.equals(op.address, op.hco.va)) {
|
|
1096
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Validator cannot sign its own transaction.", node.from.key);
|
|
1097
|
+
return Status.FAILURE;
|
|
1098
|
+
}
|
|
1099
|
+
if (b4a.equals(op.hco.in, op.hco.vn)) {
|
|
1100
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Nonces should not be the same.", node.from.key);
|
|
1101
|
+
return Status.FAILURE;
|
|
1102
|
+
}
|
|
1103
|
+
if (b4a.equals(op.hco.is, op.hco.vs)) {
|
|
1104
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Signatures should not be the same.", node.from.key);
|
|
1105
|
+
return Status.FAILURE;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const submitterAddress = addressUtils.bufferToAddress(op.address, this.#config.addressPrefix);
|
|
1109
|
+
if (submitterAddress === null) {
|
|
1110
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Submitter address is invalid.", node.from.key);
|
|
1111
|
+
return Status.FAILURE;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const submitterPublicKey = tracCryptoApi.address.decodeSafe(submitterAddress);
|
|
1115
|
+
if (!isBufferValid(submitterPublicKey, PUBLIC_KEY_LENGTH) || isZeroBuffer(submitterPublicKey)) {
|
|
1116
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Submitter public key is invalid.", node.from.key);
|
|
1117
|
+
return Status.FAILURE;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const message = createMessage(
|
|
1121
|
+
this.#config.networkId,
|
|
1122
|
+
op.hco.txv,
|
|
1123
|
+
op.hco.li,
|
|
1124
|
+
op.hco.pi,
|
|
1125
|
+
op.hco.in,
|
|
1126
|
+
OperationType.HTLC_CLAIM
|
|
1127
|
+
);
|
|
1128
|
+
if (message.length === 0) {
|
|
1129
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Invalid claim message.", node.from.key);
|
|
1130
|
+
return Status.FAILURE;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const txHash = await tracCryptoApi.hash.blake3Safe(message);
|
|
1134
|
+
if (!isBufferValid(txHash, HASH_BYTE_LENGTH)) {
|
|
1135
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Failed to hash claim message.", node.from.key);
|
|
1136
|
+
return Status.FAILURE;
|
|
1137
|
+
}
|
|
1138
|
+
if (!b4a.equals(txHash, op.hco.tx)) {
|
|
1139
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Claim transaction hash is invalid.", node.from.key);
|
|
1140
|
+
return Status.FAILURE;
|
|
1141
|
+
}
|
|
1142
|
+
if (!tracCryptoApi.signature.verify(op.hco.is, txHash, submitterPublicKey)) {
|
|
1143
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Claim submitter signature is invalid.", node.from.key);
|
|
1144
|
+
return Status.FAILURE;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const validatorAddress = addressUtils.bufferToAddress(op.hco.va, this.#config.addressPrefix);
|
|
1148
|
+
if (validatorAddress === null) {
|
|
1149
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Validator address is invalid.", node.from.key);
|
|
1150
|
+
return Status.FAILURE;
|
|
1151
|
+
}
|
|
1152
|
+
const validatorPublicKey = tracCryptoApi.address.decodeSafe(validatorAddress);
|
|
1153
|
+
if (!isBufferValid(validatorPublicKey, PUBLIC_KEY_LENGTH) || isZeroBuffer(validatorPublicKey)) {
|
|
1154
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Validator public key is invalid.", node.from.key);
|
|
1155
|
+
return Status.FAILURE;
|
|
1156
|
+
}
|
|
1157
|
+
const validatorMessage = createMessage(
|
|
1158
|
+
this.#config.networkId,
|
|
1159
|
+
op.hco.tx,
|
|
1160
|
+
op.hco.vn,
|
|
1161
|
+
OperationType.HTLC_CLAIM
|
|
1162
|
+
);
|
|
1163
|
+
if (validatorMessage.length === 0) {
|
|
1164
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Invalid validator message.", node.from.key);
|
|
1165
|
+
return Status.FAILURE;
|
|
1166
|
+
}
|
|
1167
|
+
const validatorHash = await tracCryptoApi.hash.blake3Safe(validatorMessage);
|
|
1168
|
+
if (!isBufferValid(validatorHash, HASH_BYTE_LENGTH)) {
|
|
1169
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Failed to hash validator message.", node.from.key);
|
|
1170
|
+
return Status.FAILURE;
|
|
1171
|
+
}
|
|
1172
|
+
if (!tracCryptoApi.signature.verify(op.hco.vs, validatorHash, validatorPublicKey)) {
|
|
1173
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Validator signature is invalid.", node.from.key);
|
|
1174
|
+
return Status.FAILURE;
|
|
1175
|
+
}
|
|
1176
|
+
const validatorEntry = await this.#getEntryApply(validatorAddress, batch);
|
|
1177
|
+
if (!await this.#isValidatorValidApply(validatorEntry, node, op)) {
|
|
1178
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Validator consistency check failed.", node.from.key);
|
|
1179
|
+
return Status.FAILURE;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const indexersSequenceState = await this.#getIndexerSequenceStateApply(base);
|
|
1183
|
+
if (indexersSequenceState === null || !b4a.equals(op.hco.txv, indexersSequenceState)) {
|
|
1184
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Transaction was not executed.", node.from.key);
|
|
1185
|
+
return Status.FAILURE;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const claimTxHashHex = b4a.toString(op.hco.tx, 'hex');
|
|
1189
|
+
if (await this.#getEntryApply(claimTxHashHex, batch) !== null) {
|
|
1190
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Operation has already been applied.", node.from.key);
|
|
1191
|
+
return Status.IGNORE;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const escrowKey = EntryType.HTLC_ESCROW + b4a.toString(op.hco.li, 'hex');
|
|
1195
|
+
const escrowBuffer = await this.#getEntryApply(escrowKey, batch);
|
|
1196
|
+
if (escrowBuffer === null) {
|
|
1197
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "HTLC escrow does not exist.", node.from.key);
|
|
1198
|
+
return Status.FAILURE;
|
|
1199
|
+
}
|
|
1200
|
+
const escrow = escrowEntryUtils.decode(escrowBuffer, this.#config.addressPrefix);
|
|
1201
|
+
if (!escrow || !b4a.equals(escrow.lockId, op.hco.li)) {
|
|
1202
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "HTLC escrow is invalid.", node.from.key);
|
|
1203
|
+
return Status.FAILURE;
|
|
1204
|
+
}
|
|
1205
|
+
if (escrow.status !== escrowEntryUtils.Status.PENDING) {
|
|
1206
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "HTLC escrow is already settled.", node.from.key);
|
|
1207
|
+
return Status.IGNORE;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
const currentEpoch = await this.#getEntryApply(EntryType.EPOCH_CURRENT, batch);
|
|
1211
|
+
if (!isBufferValid(currentEpoch, EPOCH_BYTE_LENGTH)) {
|
|
1212
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Current epoch is not initialized or is invalid.", node.from.key);
|
|
1213
|
+
return Status.FAILURE;
|
|
1214
|
+
}
|
|
1215
|
+
if (b4a.compare(currentEpoch, escrow.refundEpoch) >= 0) {
|
|
1216
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Claim window has expired.", node.from.key);
|
|
1217
|
+
return Status.IGNORE;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const settlement = escrowEntryUtils.makeClaim(escrowBuffer, op.hco.pi, currentEpoch, this.#config.addressPrefix);
|
|
1221
|
+
if (settlement === null) {
|
|
1222
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "HTLC preimage or escrow transition is invalid.", node.from.key);
|
|
1223
|
+
return Status.FAILURE;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const payouts = [
|
|
1227
|
+
[settlement.address, settlement.amount],
|
|
1228
|
+
[settlement.additionalFeeRecipientAddress, settlement.additionalFeeAmount],
|
|
1229
|
+
];
|
|
1230
|
+
const credits = new Map();
|
|
1231
|
+
for (const [addressBytes, amount] of payouts) {
|
|
1232
|
+
if (addressBytes === null) continue;
|
|
1233
|
+
const address = addressUtils.bufferToAddress(addressBytes, this.#config.addressPrefix);
|
|
1234
|
+
if (address === null || amount === null) {
|
|
1235
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Settlement recipient or amount is invalid.", node.from.key);
|
|
1236
|
+
return Status.FAILURE;
|
|
1237
|
+
}
|
|
1238
|
+
const total = credits.has(address) ? credits.get(address).add(amount) : amount;
|
|
1239
|
+
if (total === null) {
|
|
1240
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Settlement amount overflow.", node.from.key);
|
|
1241
|
+
return Status.FAILURE;
|
|
1242
|
+
}
|
|
1243
|
+
credits.set(address, total);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const updatedRecipients = new Map();
|
|
1247
|
+
for (const [address, amount] of credits) {
|
|
1248
|
+
const entryBuffer = await this.#getEntryApply(address, batch);
|
|
1249
|
+
let updatedEntry;
|
|
1250
|
+
if (entryBuffer === null) {
|
|
1251
|
+
updatedEntry = nodeEntryUtils.init(ZERO_WK, nodeRoleUtils.NodeRole.READER, amount.value);
|
|
1252
|
+
} else {
|
|
1253
|
+
const entry = nodeEntryUtils.decode(entryBuffer);
|
|
1254
|
+
if (entry === null) {
|
|
1255
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Settlement recipient account is invalid.", node.from.key);
|
|
1256
|
+
return Status.FAILURE;
|
|
1257
|
+
}
|
|
1258
|
+
const balance = toBalance(entry.balance);
|
|
1259
|
+
if (balance === null) {
|
|
1260
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Settlement recipient balance is invalid.", node.from.key);
|
|
1261
|
+
return Status.FAILURE;
|
|
1262
|
+
}
|
|
1263
|
+
const updatedBalance = balance.add(amount);
|
|
1264
|
+
if (updatedBalance === null) {
|
|
1265
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Settlement recipient balance overflow.", node.from.key);
|
|
1266
|
+
return Status.FAILURE;
|
|
1267
|
+
}
|
|
1268
|
+
updatedEntry = updatedBalance.update(entryBuffer);
|
|
1269
|
+
}
|
|
1270
|
+
if (!isBufferValid(updatedEntry, NODE_ENTRY_SIZE)) {
|
|
1271
|
+
this.#safeLogApply(OperationType.HTLC_CLAIM, "Failed to credit settlement recipient.", node.from.key);
|
|
1272
|
+
return Status.FAILURE;
|
|
1273
|
+
}
|
|
1274
|
+
updatedRecipients.set(address, updatedEntry);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
for (const [address, entry] of updatedRecipients) await batch.put(address, entry);
|
|
1278
|
+
await batch.put(escrowKey, settlement.claimedEscrowEntry);
|
|
1279
|
+
await batch.put(claimTxHashHex, node.value);
|
|
1280
|
+
|
|
1281
|
+
if (this.#config.enableTxApplyLogs) {
|
|
1282
|
+
console.info(`HTLC claim operation: ${claimTxHashHex} has been appended.`);
|
|
1283
|
+
}
|
|
1284
|
+
return Status.SUCCESS;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
async #handleApplyHtlcRefundOperation(op, view, base, node, batch) {
|
|
1288
|
+
if (!this.#stateValidationSchema.validateHtlcRefundOperation(op)) {
|
|
1289
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Contract schema validation failed.", node.from.key);
|
|
1290
|
+
return Status.FAILURE;
|
|
1291
|
+
}
|
|
1292
|
+
if (!Object.hasOwn(op.hro, "vs") || !Object.hasOwn(op.hro, "va") || !Object.hasOwn(op.hro, "vn")) {
|
|
1293
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Operation is not complete.", node.from.key);
|
|
1294
|
+
return Status.FAILURE;
|
|
1295
|
+
}
|
|
1296
|
+
if (b4a.equals(op.address, op.hro.va)) {
|
|
1297
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Validator cannot sign its own transaction.", node.from.key);
|
|
1298
|
+
return Status.FAILURE;
|
|
1299
|
+
}
|
|
1300
|
+
if (b4a.equals(op.hro.in, op.hro.vn)) {
|
|
1301
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Nonces should not be the same.", node.from.key);
|
|
1302
|
+
return Status.FAILURE;
|
|
1303
|
+
}
|
|
1304
|
+
if (b4a.equals(op.hro.is, op.hro.vs)) {
|
|
1305
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Signatures should not be the same.", node.from.key);
|
|
1306
|
+
return Status.FAILURE;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
const submitterAddress = addressUtils.bufferToAddress(op.address, this.#config.addressPrefix);
|
|
1310
|
+
if (submitterAddress === null) {
|
|
1311
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Submitter address is invalid.", node.from.key);
|
|
1312
|
+
return Status.FAILURE;
|
|
1313
|
+
}
|
|
1314
|
+
const submitterPublicKey = tracCryptoApi.address.decodeSafe(submitterAddress);
|
|
1315
|
+
if (!isBufferValid(submitterPublicKey, PUBLIC_KEY_LENGTH) || isZeroBuffer(submitterPublicKey)) {
|
|
1316
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Submitter public key is invalid.", node.from.key);
|
|
1317
|
+
return Status.FAILURE;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
const message = createMessage(
|
|
1321
|
+
this.#config.networkId,
|
|
1322
|
+
op.hro.txv,
|
|
1323
|
+
op.hro.li,
|
|
1324
|
+
op.hro.in,
|
|
1325
|
+
OperationType.HTLC_REFUND
|
|
1326
|
+
);
|
|
1327
|
+
if (message.length === 0) {
|
|
1328
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Invalid refund message.", node.from.key);
|
|
1329
|
+
return Status.FAILURE;
|
|
1330
|
+
}
|
|
1331
|
+
const txHash = await tracCryptoApi.hash.blake3Safe(message);
|
|
1332
|
+
if (!isBufferValid(txHash, HASH_BYTE_LENGTH)) {
|
|
1333
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Failed to hash refund message.", node.from.key);
|
|
1334
|
+
return Status.FAILURE;
|
|
1335
|
+
}
|
|
1336
|
+
if (!b4a.equals(txHash, op.hro.tx)) {
|
|
1337
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Refund transaction hash is invalid.", node.from.key);
|
|
1338
|
+
return Status.FAILURE;
|
|
1339
|
+
}
|
|
1340
|
+
if (!tracCryptoApi.signature.verify(op.hro.is, txHash, submitterPublicKey)) {
|
|
1341
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Refund submitter signature is invalid.", node.from.key);
|
|
1342
|
+
return Status.FAILURE;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const validatorAddress = addressUtils.bufferToAddress(op.hro.va, this.#config.addressPrefix);
|
|
1346
|
+
if (validatorAddress === null) {
|
|
1347
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Validator address is invalid.", node.from.key);
|
|
1348
|
+
return Status.FAILURE;
|
|
1349
|
+
}
|
|
1350
|
+
const validatorPublicKey = tracCryptoApi.address.decodeSafe(validatorAddress);
|
|
1351
|
+
if (!isBufferValid(validatorPublicKey, PUBLIC_KEY_LENGTH) || isZeroBuffer(validatorPublicKey)) {
|
|
1352
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Validator public key is invalid.", node.from.key);
|
|
1353
|
+
return Status.FAILURE;
|
|
1354
|
+
}
|
|
1355
|
+
const validatorMessage = createMessage(
|
|
1356
|
+
this.#config.networkId,
|
|
1357
|
+
op.hro.tx,
|
|
1358
|
+
op.hro.vn,
|
|
1359
|
+
OperationType.HTLC_REFUND
|
|
1360
|
+
);
|
|
1361
|
+
if (validatorMessage.length === 0) {
|
|
1362
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Invalid validator message.", node.from.key);
|
|
1363
|
+
return Status.FAILURE;
|
|
1364
|
+
}
|
|
1365
|
+
const validatorHash = await tracCryptoApi.hash.blake3Safe(validatorMessage);
|
|
1366
|
+
if (!isBufferValid(validatorHash, HASH_BYTE_LENGTH)) {
|
|
1367
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Failed to hash validator message.", node.from.key);
|
|
1368
|
+
return Status.FAILURE;
|
|
1369
|
+
}
|
|
1370
|
+
if (!tracCryptoApi.signature.verify(op.hro.vs, validatorHash, validatorPublicKey)) {
|
|
1371
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Validator signature is invalid.", node.from.key);
|
|
1372
|
+
return Status.FAILURE;
|
|
1373
|
+
}
|
|
1374
|
+
const validatorEntry = await this.#getEntryApply(validatorAddress, batch);
|
|
1375
|
+
if (!await this.#isValidatorValidApply(validatorEntry, node, op)) {
|
|
1376
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Validator consistency check failed.", node.from.key);
|
|
1377
|
+
return Status.FAILURE;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
const indexersSequenceState = await this.#getIndexerSequenceStateApply(base);
|
|
1381
|
+
if (indexersSequenceState === null || !b4a.equals(op.hro.txv, indexersSequenceState)) {
|
|
1382
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Transaction was not executed.", node.from.key);
|
|
1383
|
+
return Status.FAILURE;
|
|
395
1384
|
}
|
|
396
1385
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const startSeq = startSignedLength;
|
|
403
|
-
const endSeq = signedLength2End - 1;
|
|
404
|
-
|
|
405
|
-
if (startSeq > endSeq) {
|
|
406
|
-
throw new Error("Invalid range");
|
|
1386
|
+
const refundTxHashHex = b4a.toString(op.hro.tx, 'hex');
|
|
1387
|
+
if (await this.#getEntryApply(refundTxHashHex, batch) !== null) {
|
|
1388
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Operation has already been applied.", node.from.key);
|
|
1389
|
+
return Status.IGNORE;
|
|
407
1390
|
}
|
|
408
1391
|
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const filters = (entry) => {
|
|
415
|
-
const isPut = entry.type === "put";
|
|
416
|
-
const isHex = isHexString(entry.key);
|
|
417
|
-
const is64 = entry.key.length === 64;
|
|
418
|
-
return isPut && isHex && is64;
|
|
1392
|
+
const escrowKey = EntryType.HTLC_ESCROW + b4a.toString(op.hro.li, 'hex');
|
|
1393
|
+
const escrowBuffer = await this.#getEntryApply(escrowKey, batch);
|
|
1394
|
+
if (escrowBuffer === null) {
|
|
1395
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "HTLC escrow does not exist.", node.from.key);
|
|
1396
|
+
return Status.FAILURE;
|
|
419
1397
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
hashes.push({ hash: entry.key, confirmed_length: entry.seq });
|
|
425
|
-
}
|
|
1398
|
+
const escrow = escrowEntryUtils.decode(escrowBuffer, this.#config.addressPrefix);
|
|
1399
|
+
if (!escrow || !b4a.equals(escrow.lockId, op.hro.li)) {
|
|
1400
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "HTLC escrow is invalid.", node.from.key);
|
|
1401
|
+
return Status.FAILURE;
|
|
426
1402
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
async getRegisteredWriterKey(writingKey) {
|
|
432
|
-
const entry = await this.get(EntryType.WRITER_ADDRESS + writingKey);
|
|
433
|
-
return entry ? addressUtils.addressToBuffer(entry, this.#config.addressPrefix) : null;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
#setupHyperbee(store) {
|
|
437
|
-
this.#bee = new Hyperbee(store.get(TRAC_NAMESPACE), {
|
|
438
|
-
extension: false,
|
|
439
|
-
keyEncoding: HYPERBEE_KEY_ENCODING,
|
|
440
|
-
valueEncoding: HYPERBEE_VALUE_ENCODING,
|
|
441
|
-
})
|
|
442
|
-
return this.#bee;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// ATTENTION: DO NOT USE METHODS ABOVE IN APPLY PART!
|
|
446
|
-
///////////////////////////////APPLY////////////////////////////////////
|
|
447
|
-
|
|
448
|
-
async #apply(nodes, view, base) {
|
|
449
|
-
const batch = view.batch();
|
|
450
|
-
const batchInvoker = nodes[0].from.key;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
if (nodes.length > BATCH_SIZE) {
|
|
454
|
-
await this.#validatorPenaltyApply(batchInvoker, batch, base, OVERSIZED_BATCH_PENALTY_MULTIPLIER);
|
|
455
|
-
await batch.flush();
|
|
456
|
-
await batch.close();
|
|
457
|
-
return;
|
|
1403
|
+
if (escrow.status !== escrowEntryUtils.Status.PENDING) {
|
|
1404
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "HTLC escrow is already settled.", node.from.key);
|
|
1405
|
+
return Status.IGNORE;
|
|
458
1406
|
}
|
|
459
1407
|
|
|
460
|
-
|
|
1408
|
+
const currentEpoch = await this.#getEntryApply(EntryType.EPOCH_CURRENT, batch);
|
|
1409
|
+
if (!isBufferValid(currentEpoch, EPOCH_BYTE_LENGTH)) {
|
|
1410
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Current epoch is not initialized or is invalid.", node.from.key);
|
|
1411
|
+
return Status.FAILURE;
|
|
1412
|
+
}
|
|
461
1413
|
|
|
462
|
-
|
|
1414
|
+
if (b4a.compare(currentEpoch, escrow.refundEpoch) < 0) {
|
|
1415
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Refund epoch has not been reached.", node.from.key);
|
|
1416
|
+
return Status.IGNORE;
|
|
1417
|
+
}
|
|
463
1418
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
1419
|
+
const settlement = escrowEntryUtils.makeRefund(escrowBuffer, currentEpoch, this.#config.addressPrefix);
|
|
1420
|
+
if (settlement === null) {
|
|
1421
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "HTLC refund transition is invalid.", node.from.key);
|
|
1422
|
+
return Status.FAILURE;
|
|
1423
|
+
}
|
|
469
1424
|
|
|
470
|
-
|
|
1425
|
+
const recipientAddress = addressUtils.bufferToAddress(settlement.address, this.#config.addressPrefix);
|
|
1426
|
+
if (recipientAddress === null || settlement.amount === null) {
|
|
1427
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Refund recipient or amount is invalid.", node.from.key);
|
|
1428
|
+
return Status.FAILURE;
|
|
1429
|
+
}
|
|
471
1430
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
1431
|
+
const recipientEntryBuffer = await this.#getEntryApply(recipientAddress, batch);
|
|
1432
|
+
let updatedRecipientEntry;
|
|
1433
|
+
if (recipientEntryBuffer === null) {
|
|
1434
|
+
updatedRecipientEntry = nodeEntryUtils.init(ZERO_WK, nodeRoleUtils.NodeRole.READER, settlement.amount.value);
|
|
1435
|
+
} else {
|
|
1436
|
+
const recipientEntry = nodeEntryUtils.decode(recipientEntryBuffer);
|
|
1437
|
+
if (recipientEntry === null) {
|
|
1438
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Refund recipient account is invalid.", node.from.key);
|
|
1439
|
+
return Status.FAILURE;
|
|
476
1440
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
} else if (result !== Status.SUCCESS) {
|
|
487
|
-
this.#safeLogApply(`Unknown operation status: ${result}`, node.from.key);
|
|
488
|
-
invalidOperations++;
|
|
489
|
-
}
|
|
490
|
-
} else {
|
|
491
|
-
this.#safeLogApply(`Unknown operation type: ${op.type}`, node.from.key)
|
|
492
|
-
invalidOperations++;
|
|
1441
|
+
const recipientBalance = toBalance(recipientEntry.balance);
|
|
1442
|
+
if (recipientBalance === null) {
|
|
1443
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Refund recipient balance is invalid.", node.from.key);
|
|
1444
|
+
return Status.FAILURE;
|
|
1445
|
+
}
|
|
1446
|
+
const updatedBalance = recipientBalance.add(settlement.amount);
|
|
1447
|
+
if (updatedBalance === null) {
|
|
1448
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Refund recipient balance overflow.", node.from.key);
|
|
1449
|
+
return Status.FAILURE;
|
|
493
1450
|
}
|
|
1451
|
+
updatedRecipientEntry = updatedBalance.update(recipientEntryBuffer);
|
|
494
1452
|
}
|
|
495
|
-
if (
|
|
496
|
-
|
|
497
|
-
|
|
1453
|
+
if (!isBufferValid(updatedRecipientEntry, NODE_ENTRY_SIZE)) {
|
|
1454
|
+
this.#safeLogApply(OperationType.HTLC_REFUND, "Failed to credit refund recipient.", node.from.key);
|
|
1455
|
+
return Status.FAILURE;
|
|
498
1456
|
}
|
|
499
1457
|
|
|
500
|
-
await batch.
|
|
501
|
-
await batch.
|
|
502
|
-
|
|
1458
|
+
await batch.put(recipientAddress, updatedRecipientEntry);
|
|
1459
|
+
await batch.put(escrowKey, settlement.refundedEscrowEntry);
|
|
1460
|
+
await batch.put(refundTxHashHex, node.value);
|
|
503
1461
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
[OperationType.ADD_ADMIN]: this.#handleApplyAddAdminOperation.bind(this),
|
|
509
|
-
[OperationType.APPEND_WHITELIST]: this.#handleApplyAppendWhitelistOperation.bind(this),
|
|
510
|
-
[OperationType.ADD_WRITER]: this.#handleApplyAddWriterOperation.bind(this),
|
|
511
|
-
[OperationType.REMOVE_WRITER]: this.#handleApplyRemoveWriterOperation.bind(this),
|
|
512
|
-
[OperationType.ADMIN_RECOVERY]: this.#handleApplyAdminRecoveryOperation.bind(this),
|
|
513
|
-
[OperationType.ADD_INDEXER]: this.#handleApplyAddIndexerOperation.bind(this),
|
|
514
|
-
[OperationType.REMOVE_INDEXER]: this.#handleApplyRemoveIndexerOperation.bind(this),
|
|
515
|
-
[OperationType.BAN_VALIDATOR]: this.#handleApplyBanValidatorOperation.bind(this),
|
|
516
|
-
[OperationType.BOOTSTRAP_DEPLOYMENT]: this.#handleApplyBootstrapDeploymentOperation.bind(this),
|
|
517
|
-
[OperationType.TX]: this.#handleApplyTxOperation.bind(this),
|
|
518
|
-
[OperationType.TRANSFER]: this.#handleApplyTransferOperation.bind(this),
|
|
519
|
-
};
|
|
520
|
-
return handlers[type] || null;
|
|
1462
|
+
if (this.#config.enableTxApplyLogs) {
|
|
1463
|
+
console.info(`HTLC refund operation: ${refundTxHashHex} has been appended.`);
|
|
1464
|
+
}
|
|
1465
|
+
return Status.SUCCESS;
|
|
521
1466
|
}
|
|
522
1467
|
|
|
523
1468
|
async #handleApplyInitializeBalanceOperation(op, view, base, node, batch) {
|
|
524
|
-
if (!this.
|
|
1469
|
+
if (!this.#stateValidationSchema.validateBalanceInitialization(op)) {
|
|
525
1470
|
this.#safeLogApply(OperationType.BALANCE_INITIALIZATION, "Contract schema validation failed.", node.from.key)
|
|
526
1471
|
return Status.FAILURE;
|
|
527
1472
|
};
|
|
@@ -617,8 +1562,8 @@ class State extends ReadyResource {
|
|
|
617
1562
|
};
|
|
618
1563
|
|
|
619
1564
|
// Verify signature
|
|
620
|
-
const
|
|
621
|
-
if (!
|
|
1565
|
+
const isMessageVerified = tracCryptoApi.signature.verify(op.bio.is, hash, adminPublicKey);
|
|
1566
|
+
if (!isMessageVerified) {
|
|
622
1567
|
this.#safeLogApply(OperationType.BALANCE_INITIALIZATION, "Failed to verify message signature.", node.from.key)
|
|
623
1568
|
return Status.FAILURE;
|
|
624
1569
|
};
|
|
@@ -666,7 +1611,7 @@ class State extends ReadyResource {
|
|
|
666
1611
|
}
|
|
667
1612
|
|
|
668
1613
|
async #handleApplyDisableBalanceInitializationOperation(op, view, base, node, batch) {
|
|
669
|
-
if (!this.
|
|
1614
|
+
if (!this.#stateValidationSchema.validateCoreAdminOperation(op)) {
|
|
670
1615
|
this.#safeLogApply(OperationType.DISABLE_INITIALIZATION, "Schema validation failed.", node.from.key)
|
|
671
1616
|
return Status.FAILURE;
|
|
672
1617
|
};
|
|
@@ -739,8 +1684,8 @@ class State extends ReadyResource {
|
|
|
739
1684
|
};
|
|
740
1685
|
|
|
741
1686
|
// Verify signature
|
|
742
|
-
const
|
|
743
|
-
if (!
|
|
1687
|
+
const isMessageVerified = tracCryptoApi.signature.verify(op.cao.is, hash, adminPublicKey);
|
|
1688
|
+
if (!isMessageVerified) {
|
|
744
1689
|
this.#safeLogApply(OperationType.DISABLE_INITIALIZATION, "Failed to verify message signature.", node.from.key)
|
|
745
1690
|
return Status.FAILURE;
|
|
746
1691
|
};
|
|
@@ -764,7 +1709,7 @@ class State extends ReadyResource {
|
|
|
764
1709
|
return Status.FAILURE;
|
|
765
1710
|
};
|
|
766
1711
|
|
|
767
|
-
await batch.put(EntryType.INITIALIZATION, safeWriteUInt32BE(0
|
|
1712
|
+
await batch.put(EntryType.INITIALIZATION, safeWriteUInt32BE(0));
|
|
768
1713
|
await batch.put(txHashHexString, node.value);
|
|
769
1714
|
|
|
770
1715
|
return Status.SUCCESS;
|
|
@@ -777,7 +1722,7 @@ class State extends ReadyResource {
|
|
|
777
1722
|
INITIALIZATION, THEIR STAKED BALANCE WILL BE REDUCED (PUNISHMENT).
|
|
778
1723
|
*/
|
|
779
1724
|
|
|
780
|
-
if (!this.
|
|
1725
|
+
if (!this.#stateValidationSchema.validateCoreAdminOperation(op)) {
|
|
781
1726
|
this.#safeLogApply(OperationType.ADD_ADMIN, "Contract schema validation failed.", node.from.key)
|
|
782
1727
|
return Status.FAILURE;
|
|
783
1728
|
};
|
|
@@ -824,9 +1769,9 @@ class State extends ReadyResource {
|
|
|
824
1769
|
};
|
|
825
1770
|
|
|
826
1771
|
// verify signature
|
|
827
|
-
const
|
|
1772
|
+
const isMessageVerified = tracCryptoApi.signature.verify(op.cao.is, op.cao.tx, adminPublicKey)
|
|
828
1773
|
const txHashHexString = op.cao.tx.toString('hex');
|
|
829
|
-
if (!
|
|
1774
|
+
if (!isMessageVerified) {
|
|
830
1775
|
this.#safeLogApply(OperationType.ADD_ADMIN, "Failed to verify message signature.", node.from.key)
|
|
831
1776
|
return Status.FAILURE;
|
|
832
1777
|
};
|
|
@@ -903,7 +1848,7 @@ class State extends ReadyResource {
|
|
|
903
1848
|
|
|
904
1849
|
// initialize admin entry and initialization flag
|
|
905
1850
|
await batch.put(EntryType.ADMIN, newAdminEntry);
|
|
906
|
-
await batch.put(EntryType.INITIALIZATION, safeWriteUInt32BE(1
|
|
1851
|
+
await batch.put(EntryType.INITIALIZATION, safeWriteUInt32BE(1));
|
|
907
1852
|
await batch.put(txHashHexString, node.value);
|
|
908
1853
|
|
|
909
1854
|
if (this.#config.enableTxApplyLogs) {
|
|
@@ -914,7 +1859,7 @@ class State extends ReadyResource {
|
|
|
914
1859
|
}
|
|
915
1860
|
|
|
916
1861
|
async #handleApplyAdminRecoveryOperation(op, view, base, node, batch) {
|
|
917
|
-
if (!this.
|
|
1862
|
+
if (!this.#stateValidationSchema.validateRoleAccessOperation(op)) {
|
|
918
1863
|
this.#safeLogApply(OperationType.ADMIN_RECOVERY, "Contract schema validation failed.", node.from.key)
|
|
919
1864
|
return Status.FAILURE;
|
|
920
1865
|
};
|
|
@@ -1165,7 +2110,7 @@ class State extends ReadyResource {
|
|
|
1165
2110
|
}
|
|
1166
2111
|
|
|
1167
2112
|
async #handleApplyAppendWhitelistOperation(op, view, base, node, batch) {
|
|
1168
|
-
if (!this.
|
|
2113
|
+
if (!this.#stateValidationSchema.validateAdminControlOperation(op)) {
|
|
1169
2114
|
this.#safeLogApply(OperationType.APPEND_WHITELIST, "Contract schema validation failed.", node.from.key)
|
|
1170
2115
|
return Status.FAILURE;
|
|
1171
2116
|
};
|
|
@@ -1414,7 +2359,7 @@ class State extends ReadyResource {
|
|
|
1414
2359
|
}
|
|
1415
2360
|
|
|
1416
2361
|
async #handleApplyAddWriterOperation(op, view, base, node, batch) {
|
|
1417
|
-
if (!this.
|
|
2362
|
+
if (!this.#stateValidationSchema.validateRoleAccessOperation(op)) {
|
|
1418
2363
|
this.#safeLogApply(OperationType.ADD_WRITER, "Contract schema validation failed.", node.from.key)
|
|
1419
2364
|
return Status.FAILURE;
|
|
1420
2365
|
};
|
|
@@ -1718,7 +2663,7 @@ class State extends ReadyResource {
|
|
|
1718
2663
|
}
|
|
1719
2664
|
|
|
1720
2665
|
async #handleApplyRemoveWriterOperation(op, view, base, node, batch) {
|
|
1721
|
-
if (!this.
|
|
2666
|
+
if (!this.#stateValidationSchema.validateRoleAccessOperation(op)) {
|
|
1722
2667
|
this.#safeLogApply(OperationType.REMOVE_WRITER, "Contract schema validation failed.", node.from.key)
|
|
1723
2668
|
return Status.FAILURE;
|
|
1724
2669
|
};
|
|
@@ -1982,7 +2927,7 @@ class State extends ReadyResource {
|
|
|
1982
2927
|
}
|
|
1983
2928
|
|
|
1984
2929
|
async #handleApplyAddIndexerOperation(op, view, base, node, batch) {
|
|
1985
|
-
if (!this.
|
|
2930
|
+
if (!this.#stateValidationSchema.validateAdminControlOperation(op)) {
|
|
1986
2931
|
this.#safeLogApply(OperationType.ADD_INDEXER, "Contract schema validation failed.", node.from.key)
|
|
1987
2932
|
return Status.FAILURE;
|
|
1988
2933
|
};
|
|
@@ -2068,9 +3013,9 @@ class State extends ReadyResource {
|
|
|
2068
3013
|
return Status.FAILURE;
|
|
2069
3014
|
};
|
|
2070
3015
|
|
|
2071
|
-
const
|
|
3016
|
+
const isMessageVerified = tracCryptoApi.signature.verify(op.aco.is, hash, adminPublicKey);
|
|
2072
3017
|
const txHashHexString = hash.toString('hex');
|
|
2073
|
-
if (!
|
|
3018
|
+
if (!isMessageVerified) {
|
|
2074
3019
|
this.#safeLogApply(OperationType.ADD_INDEXER, "Failed to verify message signature.", node.from.key)
|
|
2075
3020
|
return Status.FAILURE;
|
|
2076
3021
|
};
|
|
@@ -2200,7 +3145,7 @@ class State extends ReadyResource {
|
|
|
2200
3145
|
}
|
|
2201
3146
|
|
|
2202
3147
|
async #handleApplyRemoveIndexerOperation(op, view, base, node, batch) {
|
|
2203
|
-
if (!this.
|
|
3148
|
+
if (!this.#stateValidationSchema.validateAdminControlOperation(op)) {
|
|
2204
3149
|
this.#safeLogApply(OperationType.REMOVE_INDEXER, "Contract schema validation failed.", node.from.key)
|
|
2205
3150
|
return Status.FAILURE;
|
|
2206
3151
|
};
|
|
@@ -2283,9 +3228,9 @@ class State extends ReadyResource {
|
|
|
2283
3228
|
return Status.FAILURE;
|
|
2284
3229
|
};
|
|
2285
3230
|
|
|
2286
|
-
const
|
|
3231
|
+
const isMessageVerified = tracCryptoApi.signature.verify(op.aco.is, hash, adminPublicKey);
|
|
2287
3232
|
const txHashHexString = hash.toString('hex');
|
|
2288
|
-
if (!
|
|
3233
|
+
if (!isMessageVerified) {
|
|
2289
3234
|
this.#safeLogApply(OperationType.REMOVE_INDEXER, "Failed to verify message signature.", node.from.key)
|
|
2290
3235
|
return Status.FAILURE;
|
|
2291
3236
|
};
|
|
@@ -2414,10 +3359,13 @@ class State extends ReadyResource {
|
|
|
2414
3359
|
if (this.#config.enableTxApplyLogs) {
|
|
2415
3360
|
console.info(`Indexer has been removed addr:wk:tx - ${toRemoveAddressString}:${decodedNodeEntry.wk.toString('hex')}:${txHashHexString}`);
|
|
2416
3361
|
}
|
|
3362
|
+
|
|
3363
|
+
this.#emitEvent(CustomEventType.IS_NON_INDEXER, tracCryptoApi.address.decodeSafe(toRemoveAddressString))
|
|
3364
|
+
return Status.SUCCESS;
|
|
2417
3365
|
}
|
|
2418
3366
|
|
|
2419
3367
|
async #handleApplyBanValidatorOperation(op, view, base, node, batch) {
|
|
2420
|
-
if (!this.
|
|
3368
|
+
if (!this.#stateValidationSchema.validateAdminControlOperation(op)) {
|
|
2421
3369
|
this.#safeLogApply(OperationType.BAN_VALIDATOR, "Contract schema validation failed.", node.from.key)
|
|
2422
3370
|
return Status.FAILURE;
|
|
2423
3371
|
};
|
|
@@ -2486,9 +3434,9 @@ class State extends ReadyResource {
|
|
|
2486
3434
|
return Status.FAILURE;
|
|
2487
3435
|
};
|
|
2488
3436
|
|
|
2489
|
-
const
|
|
3437
|
+
const isMessageVerified = tracCryptoApi.signature.verify(op.aco.is, regeneratedHash, adminPublicKey);
|
|
2490
3438
|
const txHashHexString = regeneratedHash.toString('hex');
|
|
2491
|
-
if (!
|
|
3439
|
+
if (!isMessageVerified) {
|
|
2492
3440
|
this.#safeLogApply(OperationType.BAN_VALIDATOR, "Failed to verify message signature.", node.from.key)
|
|
2493
3441
|
return Status.FAILURE;
|
|
2494
3442
|
}
|
|
@@ -2618,7 +3566,7 @@ class State extends ReadyResource {
|
|
|
2618
3566
|
}
|
|
2619
3567
|
|
|
2620
3568
|
async #handleApplyBootstrapDeploymentOperation(op, view, base, node, batch) {
|
|
2621
|
-
if (!this.
|
|
3569
|
+
if (!this.#stateValidationSchema.validateBootstrapDeploymentOperation(op)) {
|
|
2622
3570
|
this.#safeLogApply(OperationType.BOOTSTRAP_DEPLOYMENT, "Contract schema validation failed.", node.from.key)
|
|
2623
3571
|
return Status.FAILURE;
|
|
2624
3572
|
};
|
|
@@ -2852,7 +3800,7 @@ class State extends ReadyResource {
|
|
|
2852
3800
|
|
|
2853
3801
|
async #handleApplyTxOperation(op, view, base, node, batch) {
|
|
2854
3802
|
// ATTENTION: The sanitization should be done before ANY other check, otherwise we risk crashing
|
|
2855
|
-
if (!this.
|
|
3803
|
+
if (!this.#stateValidationSchema.validateTransactionOperation(op)) {
|
|
2856
3804
|
this.#safeLogApply(OperationType.TX, "Contract schema validation failed.", node.from.key)
|
|
2857
3805
|
return Status.FAILURE;
|
|
2858
3806
|
};
|
|
@@ -3066,7 +4014,7 @@ class State extends ReadyResource {
|
|
|
3066
4014
|
}
|
|
3067
4015
|
|
|
3068
4016
|
async #handleApplyTransferOperation(op, view, base, node, batch) {
|
|
3069
|
-
if (!this.
|
|
4017
|
+
if (!this.#stateValidationSchema.validateTransferOperation(op)) {
|
|
3070
4018
|
this.#safeLogApply(OperationType.TRANSFER, "Contract schema validation failed.", node.from.key)
|
|
3071
4019
|
return Status.FAILURE;
|
|
3072
4020
|
};
|
|
@@ -3103,7 +4051,7 @@ class State extends ReadyResource {
|
|
|
3103
4051
|
if (b4a.equals(requesterPublicKey, NULL_BUFFER)) {
|
|
3104
4052
|
this.#safeLogApply(OperationType.TRANSFER, "Error while decoding requester public key.", node.from.key)
|
|
3105
4053
|
return Status.FAILURE;
|
|
3106
|
-
}
|
|
4054
|
+
}
|
|
3107
4055
|
|
|
3108
4056
|
// recreate requester message
|
|
3109
4057
|
const requesterMessage = createMessage(
|
|
@@ -3257,18 +4205,302 @@ class State extends ReadyResource {
|
|
|
3257
4205
|
await batch.put(requesterAddressString, transferResult.senderEntry);
|
|
3258
4206
|
await batch.put(validatorAddressString, transferResult.validatorEntry);
|
|
3259
4207
|
|
|
3260
|
-
if (!isSelfTransfer && !isRecipientValidator && transferResult.recipientEntry !== null) {
|
|
3261
|
-
await batch.put(recipientAddressString, transferResult.recipientEntry);
|
|
4208
|
+
if (!isSelfTransfer && !isRecipientValidator && transferResult.recipientEntry !== null) {
|
|
4209
|
+
await batch.put(recipientAddressString, transferResult.recipientEntry);
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
await batch.put(hashHexString, node.value);
|
|
4213
|
+
|
|
4214
|
+
if (this.#config.enableTxApplyLogs) {
|
|
4215
|
+
console.info(`Transfer operation: ${hashHexString} has been appended.`);
|
|
4216
|
+
}
|
|
4217
|
+
return Status.SUCCESS;
|
|
4218
|
+
}
|
|
4219
|
+
|
|
4220
|
+
async #handleApplySetEpochOperation(op, _view, base, node, batch) {
|
|
4221
|
+
if (!this.#stateValidationSchema.validateSetEpochOperation(op)) {
|
|
4222
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Contract schema validation failed.", node.from.key)
|
|
4223
|
+
return Status.FAILURE;
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
const currentConsensusConfigIndexBuffer = await this.#getEntryApply(EntryType.CONSENSUS_CONFIG_CURRENT, batch);
|
|
4227
|
+
if (currentConsensusConfigIndexBuffer === null) {
|
|
4228
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Consensus config is not initialized.", node.from.key)
|
|
4229
|
+
return Status.FAILURE;
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
if (currentConsensusConfigIndexBuffer.length !== CONSENSUS_CONFIG_INDEX_SIZE) {
|
|
4233
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Consensus config data is malformed or corrupted.", node.from.key)
|
|
4234
|
+
return Status.FAILURE;
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4237
|
+
const currentConsensusConfigIndex = safeReadUint32BE(currentConsensusConfigIndexBuffer);
|
|
4238
|
+
const consensusConfigBuffer = await this.#getEntryApply(
|
|
4239
|
+
EntryType.CONSENSUS_CONFIG_RECORD + currentConsensusConfigIndex,
|
|
4240
|
+
batch
|
|
4241
|
+
);
|
|
4242
|
+
if (consensusConfigBuffer === null) {
|
|
4243
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Consensus config record does not exist.", node.from.key)
|
|
4244
|
+
return Status.FAILURE;
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
const consensusConfig = safeDecodeConsensusConfig(consensusConfigBuffer);
|
|
4248
|
+
if (consensusConfig === null) {
|
|
4249
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Failed to decode consensus config.", node.from.key)
|
|
4250
|
+
return Status.FAILURE;
|
|
4251
|
+
}
|
|
4252
|
+
|
|
4253
|
+
const consensusSchemaVersion = safeReadUint8(consensusConfig.sv);
|
|
4254
|
+
const epochSchemaVersion = safeReadUint8(op.seo.sv);
|
|
4255
|
+
if (consensusSchemaVersion !== epochSchemaVersion) {
|
|
4256
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Epoch schema version does not match the current consensus config.", node.from.key)
|
|
4257
|
+
return Status.FAILURE;
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
// Use the config at this point in replay, including earlier updates in this batch.
|
|
4261
|
+
switch (consensusSchemaVersion) {
|
|
4262
|
+
case ConsensusVersion.VDF_V1:
|
|
4263
|
+
return await this.#applyVdfV1Epoch(op, consensusConfig, base, node, batch);
|
|
4264
|
+
default:
|
|
4265
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Unsupported epoch schema version.", node.from.key)
|
|
4266
|
+
return Status.FAILURE;
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
async #applyVdfV1Epoch(op, consensusConfig, base, node, batch) {
|
|
4271
|
+
const epochProof = safeDecodeEpochProofV1(op.seo.data);
|
|
4272
|
+
if (epochProof === null) {
|
|
4273
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Failed to decode epoch data.", node.from.key)
|
|
4274
|
+
return Status.FAILURE;
|
|
4275
|
+
}
|
|
4276
|
+
if (!this.#stateValidationSchema.validateEpochProofV1(epochProof)) {
|
|
4277
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Epoch data schema validation failed.", node.from.key)
|
|
4278
|
+
return Status.FAILURE;
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4281
|
+
const { pd: encodedProofProposal, app: encodedApprovals } = epochProof;
|
|
4282
|
+
|
|
4283
|
+
const requesterAddressBuffer = op.address;
|
|
4284
|
+
const requesterAddressString = addressUtils.bufferToAddress(requesterAddressBuffer, this.#config.addressPrefix);
|
|
4285
|
+
if (requesterAddressString === null) {
|
|
4286
|
+
this.#safeLogApply(OperationType.BOOTSTRAP_DEPLOYMENT, "Requester address is invalid.", node.from.key)
|
|
4287
|
+
return Status.FAILURE;
|
|
4288
|
+
}
|
|
4289
|
+
|
|
4290
|
+
// validate requester public key
|
|
4291
|
+
const requesterPublicKey = tracCryptoApi.address.decodeSafe(requesterAddressString);
|
|
4292
|
+
if (b4a.equals(requesterPublicKey, NULL_BUFFER)) {
|
|
4293
|
+
this.#safeLogApply(OperationType.BOOTSTRAP_DEPLOYMENT, "Failed to decode requester public key.", node.from.key)
|
|
4294
|
+
return Status.FAILURE;
|
|
4295
|
+
}
|
|
4296
|
+
|
|
4297
|
+
const proofProposal = safeDecodeProofProposal(encodedProofProposal);
|
|
4298
|
+
if (proofProposal === null) {
|
|
4299
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Failed to decode proof proposal.", node.from.key)
|
|
4300
|
+
return Status.FAILURE;
|
|
4301
|
+
}
|
|
4302
|
+
|
|
4303
|
+
const currentEpochBuffer = await this.#getEntryApply(EntryType.EPOCH_CURRENT, batch);
|
|
4304
|
+
if (currentEpochBuffer === null) {
|
|
4305
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Current epoch is not initialized. Genesis epoch has not been set.", node.from.key)
|
|
4306
|
+
return Status.FAILURE;
|
|
4307
|
+
}
|
|
4308
|
+
|
|
4309
|
+
const nextEpochBuffer = incrementBuffer(currentEpochBuffer, EPOCH_BYTE_LENGTH);
|
|
4310
|
+
if (nextEpochBuffer === null) {
|
|
4311
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Current epoch index value is invalid or corrupted", node.from.key)
|
|
4312
|
+
return Status.FAILURE;
|
|
4313
|
+
}
|
|
4314
|
+
|
|
4315
|
+
const proposedEpochBuffer = proofProposal.epoch;
|
|
4316
|
+
const epochIndexCompare = b4a.compare(nextEpochBuffer, proposedEpochBuffer)
|
|
4317
|
+
|
|
4318
|
+
// String helpers
|
|
4319
|
+
const currentEpochStr = safeToUIntString(currentEpochBuffer);
|
|
4320
|
+
const nextEpochStr = safeToUIntString(nextEpochBuffer);
|
|
4321
|
+
if (nextEpochStr === null || currentEpochStr === null) {
|
|
4322
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Epoch index data is malformed or corrupted.", node.from.key)
|
|
4323
|
+
return Status.FAILURE;
|
|
4324
|
+
}
|
|
4325
|
+
|
|
4326
|
+
if (epochIndexCompare > 0) { // proposedEpoch < nextEpoch
|
|
4327
|
+
this.#safeLogApply(OperationType.SET_EPOCH, `Stale epoch proposal. Epoch ${currentEpochStr} is already committed.`, node.from.key)
|
|
4328
|
+
return Status.IGNORE;
|
|
4329
|
+
}
|
|
4330
|
+
|
|
4331
|
+
if (epochIndexCompare < 0) { // proposedEpoch > nextEpoch
|
|
4332
|
+
this.#safeLogApply(OperationType.SET_EPOCH, `Unexpected epoch. Proposal must target epoch ${nextEpochStr} but got ${safeToUIntString(proposedEpochBuffer)}.`, node.from.key)
|
|
4333
|
+
return Status.FAILURE;
|
|
4334
|
+
}
|
|
4335
|
+
|
|
4336
|
+
const expectedNetworkId = uint16ToBuffer(this.#config.networkId);
|
|
4337
|
+
if (!b4a.equals(proofProposal.network_id, expectedNetworkId)) {
|
|
4338
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Invalid proof proposal network id.", node.from.key)
|
|
4339
|
+
return Status.FAILURE;
|
|
4340
|
+
}
|
|
4341
|
+
|
|
4342
|
+
const currentEpochHash = await this.#getEntryApply(EntryType.EPOCH + currentEpochStr, batch);
|
|
4343
|
+
if (currentEpochHash === null || !b4a.equals(currentEpochHash, proofProposal.previous_epoch_record_hash)) {
|
|
4344
|
+
this.#safeLogApply(OperationType.SET_EPOCH, `Previous epoch record hash mismatch for epoch ${currentEpochStr}.`, node.from.key)
|
|
4345
|
+
return Status.FAILURE;
|
|
4346
|
+
}
|
|
4347
|
+
|
|
4348
|
+
const decodedVdfParams = safeDecodeVdfConfig(consensusConfig.cd);
|
|
4349
|
+
if (decodedVdfParams === null) {
|
|
4350
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Invalid VDF params value.", node.from.key)
|
|
4351
|
+
return Status.FAILURE;
|
|
4352
|
+
}
|
|
4353
|
+
|
|
4354
|
+
const { difficulty, discriminantBitSize } = decodedVdfParams;
|
|
4355
|
+
if (
|
|
4356
|
+
!b4a.equals(difficulty, proofProposal.difficulty) ||
|
|
4357
|
+
!b4a.equals(discriminantBitSize, proofProposal.discriminant_bit_size)
|
|
4358
|
+
) {
|
|
4359
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "VDF parameters do not match the current consensus config.", node.from.key)
|
|
4360
|
+
return Status.FAILURE;
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4363
|
+
const indexers = Object.values(base.system.indexers);
|
|
4364
|
+
const indexerAddresses = new Set();
|
|
4365
|
+
for (const indexer of indexers) {
|
|
4366
|
+
const indexerAddressBuffer = await this.#getRegisteredWriterKeyApply(batch, indexer.key.toString('hex'));
|
|
4367
|
+
if (!indexerAddressBuffer) continue;
|
|
4368
|
+
const indexerAddress = addressUtils.bufferToAddress(indexerAddressBuffer, this.#config.addressPrefix);
|
|
4369
|
+
if (indexerAddress) indexerAddresses.add(indexerAddress);
|
|
4370
|
+
}
|
|
4371
|
+
|
|
4372
|
+
const proposerAddress = addressUtils.bufferToAddress(proofProposal.proposer, this.#config.addressPrefix);
|
|
4373
|
+
if (!proposerAddress || !indexerAddresses.has(proposerAddress)) {
|
|
4374
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Proposer is not a registered indexer.", node.from.key)
|
|
4375
|
+
return Status.FAILURE;
|
|
4376
|
+
}
|
|
4377
|
+
|
|
4378
|
+
const indexerCount = indexers.length;
|
|
4379
|
+
const quorumThreshold = indexerCount <= 2 ? 1 : Math.floor(indexerCount / 2) + 1;
|
|
4380
|
+
const approvalCount = 1 + encodedApprovals.length; // proposer plus submitted approvals (proposer's own verified signature counts as one approval)
|
|
4381
|
+
|
|
4382
|
+
if (approvalCount < quorumThreshold) {
|
|
4383
|
+
this.#safeLogApply(OperationType.SET_EPOCH, `Insufficient submitted approvals for quorum. Required ${quorumThreshold}, got ${approvalCount}.`, node.from.key)
|
|
4384
|
+
return Status.FAILURE;
|
|
4385
|
+
}
|
|
4386
|
+
|
|
4387
|
+
const challengeData = createMessage(
|
|
4388
|
+
proofProposal.network_id,
|
|
4389
|
+
proofProposal.epoch,
|
|
4390
|
+
proofProposal.previous_epoch_record_hash,
|
|
4391
|
+
proofProposal.proposer,
|
|
4392
|
+
proofProposal.difficulty,
|
|
4393
|
+
proofProposal.discriminant_bit_size
|
|
4394
|
+
);
|
|
4395
|
+
|
|
4396
|
+
const proposerPublicKey = tracCryptoApi.address.decodeSafe(proposerAddress);
|
|
4397
|
+
let proposalSignatureVerified = false;
|
|
4398
|
+
if (!b4a.equals(proposerPublicKey, NULL_BUFFER)) {
|
|
4399
|
+
const proposalMessage = createMessage(challengeData, proofProposal.proof);
|
|
4400
|
+
const proposalHash = await tracCryptoApi.hash.blake3Safe(proposalMessage);
|
|
4401
|
+
proposalSignatureVerified = tracCryptoApi.signature.verify(proofProposal.signature, proposalHash, proposerPublicKey);
|
|
4402
|
+
}
|
|
4403
|
+
if (!proposalSignatureVerified) {
|
|
4404
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Failed to verify proof proposal signature.", node.from.key)
|
|
4405
|
+
return Status.FAILURE;
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
const approvalErrorMessage = await this.#validateEpochApprovals(
|
|
4409
|
+
encodedApprovals,
|
|
4410
|
+
indexerAddresses,
|
|
4411
|
+
proposerAddress,
|
|
4412
|
+
challengeData,
|
|
4413
|
+
proofProposal
|
|
4414
|
+
);
|
|
4415
|
+
if (approvalErrorMessage) {
|
|
4416
|
+
this.#safeLogApply(OperationType.SET_EPOCH, approvalErrorMessage, node.from.key)
|
|
4417
|
+
return Status.FAILURE;
|
|
4418
|
+
}
|
|
4419
|
+
|
|
4420
|
+
let vdfProofVerified = false;
|
|
4421
|
+
try {
|
|
4422
|
+
vdfProofVerified = await verifyWesolowski(
|
|
4423
|
+
challengeData,
|
|
4424
|
+
difficulty.readUInt32BE(0),
|
|
4425
|
+
proofProposal.proof,
|
|
4426
|
+
discriminantBitSize.readUInt16BE(0)
|
|
4427
|
+
);
|
|
4428
|
+
} catch (error) {
|
|
4429
|
+
console.error(error);
|
|
4430
|
+
}
|
|
4431
|
+
if (!vdfProofVerified) {
|
|
4432
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "VDF proof is invalid.", node.from.key)
|
|
4433
|
+
return Status.FAILURE;
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4436
|
+
const encodedEpochProof = safeEncodeEpochProofV1(epochProof);
|
|
4437
|
+
if (encodedEpochProof.length === 0) {
|
|
4438
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Failed to encode epoch proof.", node.from.key)
|
|
4439
|
+
return Status.FAILURE;
|
|
4440
|
+
}
|
|
4441
|
+
|
|
4442
|
+
const encodedEpochRecord = safeEncodeEpochRecord({ sv: op.seo.sv, data: encodedEpochProof });
|
|
4443
|
+
if (encodedEpochRecord.length === 0) {
|
|
4444
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Failed to encode epoch record.", node.from.key)
|
|
4445
|
+
return Status.FAILURE;
|
|
4446
|
+
}
|
|
4447
|
+
|
|
4448
|
+
const epochProofHash = await tracCryptoApi.hash.blake3Safe(encodedEpochRecord);
|
|
4449
|
+
if (!isBufferValid(epochProofHash, HASH_BYTE_LENGTH)) {
|
|
4450
|
+
this.#safeLogApply(OperationType.SET_EPOCH, "Failed to hash epoch proof.", node.from.key)
|
|
4451
|
+
return Status.FAILURE;
|
|
3262
4452
|
}
|
|
3263
4453
|
|
|
3264
|
-
await batch.put(
|
|
4454
|
+
await batch.put(EntryType.EPOCH_CURRENT, nextEpochBuffer);
|
|
4455
|
+
await batch.put(EntryType.EPOCH + nextEpochStr, epochProofHash);
|
|
4456
|
+
await batch.put(EntryType.EPOCH_HASH + epochProofHash.toString('hex'), encodedEpochRecord);
|
|
3265
4457
|
|
|
3266
4458
|
if (this.#config.enableTxApplyLogs) {
|
|
3267
|
-
console.info(`
|
|
4459
|
+
console.info(`Epoch ${nextEpochStr} committed. proposer:approvals - ${proposerAddress}:${encodedApprovals.length + 1}`); // We should account for the proposer approval too, hence the '+ 1' at the end
|
|
3268
4460
|
}
|
|
4461
|
+
|
|
4462
|
+
this.#emitEvent(CustomEventType.EPOCH_CREATED, { epoch: b4a.from(nextEpochBuffer), proposerAddress }); // notify epoch committed
|
|
3269
4463
|
return Status.SUCCESS;
|
|
3270
4464
|
}
|
|
3271
4465
|
|
|
4466
|
+
async #validateEpochApprovals(encodedApprovals, indexerAddresses, proposerAddress, challengeData, proofProposal) {
|
|
4467
|
+
const approverIdentities = new Set();
|
|
4468
|
+
|
|
4469
|
+
for (const encodedApproval of encodedApprovals) {
|
|
4470
|
+
const approval = safeDecodeProofProposalApproval(encodedApproval);
|
|
4471
|
+
if (approval === null) return 'Failed to decode epoch approval.';
|
|
4472
|
+
|
|
4473
|
+
const approverAddress = addressUtils.bufferToAddress(approval.approver, this.#config.addressPrefix);
|
|
4474
|
+
if (!approverAddress) return 'Epoch approval contains an invalid approver.';
|
|
4475
|
+
if (approverAddress === proposerAddress) return 'Proposer cannot approve its own epoch proposal.';
|
|
4476
|
+
if (!indexerAddresses.has(approverAddress)) return 'Epoch approver is not a registered indexer.';
|
|
4477
|
+
if (approverIdentities.has(approverAddress)) return 'Epoch approval contains a duplicate approver.';
|
|
4478
|
+
|
|
4479
|
+
const approverPublicKey = tracCryptoApi.address.decodeSafe(approverAddress);
|
|
4480
|
+
if (b4a.equals(approverPublicKey, NULL_BUFFER)) return 'Failed to decode epoch approver public key.';
|
|
4481
|
+
|
|
4482
|
+
const approvalMessage = createMessage(
|
|
4483
|
+
challengeData,
|
|
4484
|
+
proofProposal.proof,
|
|
4485
|
+
approval.approver,
|
|
4486
|
+
proofProposal.signature
|
|
4487
|
+
);
|
|
4488
|
+
let approvalVerified = false;
|
|
4489
|
+
const approvalHash = await tracCryptoApi.hash.blake3Safe(approvalMessage);
|
|
4490
|
+
approvalVerified = tracCryptoApi.signature.verify(
|
|
4491
|
+
approval.approval_sig,
|
|
4492
|
+
approvalHash,
|
|
4493
|
+
approverPublicKey
|
|
4494
|
+
);
|
|
4495
|
+
|
|
4496
|
+
if (!approvalVerified) return 'Failed to verify epoch approval signature.';
|
|
4497
|
+
|
|
4498
|
+
approverIdentities.add(approverAddress);
|
|
4499
|
+
}
|
|
4500
|
+
|
|
4501
|
+
return null;
|
|
4502
|
+
}
|
|
4503
|
+
|
|
3272
4504
|
async #transfer(senderAddressString, recipientAddressString, validatorAddressString, validatorEntryBuffer, transferAmountBuffer, feeAmountBuffer, isSelfTransfer, isRecipientValidator, batch, node) {
|
|
3273
4505
|
if (!senderAddressString ||
|
|
3274
4506
|
!recipientAddressString ||
|
|
@@ -3456,6 +4688,7 @@ class State extends ReadyResource {
|
|
|
3456
4688
|
return null
|
|
3457
4689
|
}
|
|
3458
4690
|
}
|
|
4691
|
+
|
|
3459
4692
|
async #isValidatorValidApply(validatorEntryBuffer, node, op) {
|
|
3460
4693
|
// TODO: Maybe we should transfer all validator checks to this function (address, pubKey, signature, etc)
|
|
3461
4694
|
if (validatorEntryBuffer === null) {
|
|
@@ -3502,7 +4735,7 @@ class State extends ReadyResource {
|
|
|
3502
4735
|
if (initialization === null) {
|
|
3503
4736
|
return false
|
|
3504
4737
|
} else {
|
|
3505
|
-
return b4a.equals(initialization, safeWriteUInt32BE(0
|
|
4738
|
+
return b4a.equals(initialization, safeWriteUInt32BE(0))
|
|
3506
4739
|
}
|
|
3507
4740
|
}
|
|
3508
4741
|
|
|
@@ -3781,12 +5014,6 @@ class State extends ReadyResource {
|
|
|
3781
5014
|
return { newLicenseLength, decodedNewLicenseLength };
|
|
3782
5015
|
}
|
|
3783
5016
|
|
|
3784
|
-
#emitEvent(event, ...args) {
|
|
3785
|
-
try {
|
|
3786
|
-
this.emit(event, ...args)
|
|
3787
|
-
} catch (_ignored) { }
|
|
3788
|
-
}
|
|
3789
|
-
|
|
3790
5017
|
async #transferFeeTxOperation(requesterAddressString, validatorAddressString, validatorEntryBuffer, subnetworkCreatorAddressString, feeAmount, batch, node) {
|
|
3791
5018
|
if (!requesterAddressString ||
|
|
3792
5019
|
!validatorAddressString ||
|
|
@@ -3940,6 +5167,400 @@ class State extends ReadyResource {
|
|
|
3940
5167
|
};
|
|
3941
5168
|
}
|
|
3942
5169
|
|
|
5170
|
+
#validateConsensusConfigApply(consensusConfig) {
|
|
5171
|
+
if (!isBufferValid(consensusConfig?.sv, CONSENSUS_CONFIG_SCHEMA_VERSION_BYTE_LENGTH)) {
|
|
5172
|
+
return false;
|
|
5173
|
+
}
|
|
5174
|
+
|
|
5175
|
+
switch (safeReadUint8(consensusConfig.sv)) {
|
|
5176
|
+
case ConsensusVersion.VDF_V1: {
|
|
5177
|
+
const configData = safeDecodeVdfConfig(consensusConfig.cd);
|
|
5178
|
+
if (configData === null) return false;
|
|
5179
|
+
|
|
5180
|
+
return !isZeroBuffer(configData.difficulty) &&
|
|
5181
|
+
Object.hasOwn(VDF_PROOF_BYTE_LENGTHS, configData.discriminantBitSize.readUInt16BE(0));
|
|
5182
|
+
}
|
|
5183
|
+
default:
|
|
5184
|
+
return false;
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
|
|
5188
|
+
async #handleApplySetGenesisEpoch(op, view, base, node, batch) {
|
|
5189
|
+
if (!this.#stateValidationSchema.validateConsensusControlOperation(op)) {
|
|
5190
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Contract schema validation failed.", node.from.key)
|
|
5191
|
+
return Status.FAILURE;
|
|
5192
|
+
}
|
|
5193
|
+
|
|
5194
|
+
// Extract and validate the requester address (admin)
|
|
5195
|
+
const requesterAddressBuffer = op.address;
|
|
5196
|
+
const requesterAddressString = addressUtils.bufferToAddress(requesterAddressBuffer, this.#config.addressPrefix);
|
|
5197
|
+
if (requesterAddressString === null) {
|
|
5198
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Requester address is invalid.", node.from.key)
|
|
5199
|
+
return Status.FAILURE;
|
|
5200
|
+
}
|
|
5201
|
+
|
|
5202
|
+
// Validate requester public key
|
|
5203
|
+
const requesterPublicKey = tracCryptoApi.address.decodeSafe(requesterAddressString);
|
|
5204
|
+
if (b4a.equals(requesterPublicKey, NULL_BUFFER)) {
|
|
5205
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Failed to decode requester public key.", node.from.key)
|
|
5206
|
+
return Status.FAILURE;
|
|
5207
|
+
}
|
|
5208
|
+
// ensure that an admin invoked this operation
|
|
5209
|
+
const adminEntry = await this.#getEntryApply(EntryType.ADMIN, batch);
|
|
5210
|
+
if (adminEntry === null) {
|
|
5211
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Invalid admin entry.", node.from.key)
|
|
5212
|
+
return Status.FAILURE;
|
|
5213
|
+
}
|
|
5214
|
+
|
|
5215
|
+
const decodedAdminEntry = adminEntryUtils.decode(adminEntry, this.#config.addressPrefix);
|
|
5216
|
+
if (decodedAdminEntry === null) {
|
|
5217
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Failed to decode admin entry.", node.from.key)
|
|
5218
|
+
return Status.FAILURE;
|
|
5219
|
+
}
|
|
5220
|
+
|
|
5221
|
+
if (!this.#isAdminApply(decodedAdminEntry, node)) {
|
|
5222
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Node is not allowed to perform this operation. (ADMIN ONLY)", node.from.key)
|
|
5223
|
+
return Status.FAILURE;
|
|
5224
|
+
}
|
|
5225
|
+
|
|
5226
|
+
// Extract admin public key
|
|
5227
|
+
const adminPublicKey = tracCryptoApi.address.decodeSafe(decodedAdminEntry.address);
|
|
5228
|
+
if (b4a.equals(adminPublicKey, NULL_BUFFER)) {
|
|
5229
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Failed to decode admin public key.", node.from.key)
|
|
5230
|
+
return Status.FAILURE;
|
|
5231
|
+
}
|
|
5232
|
+
// Admin consistency check
|
|
5233
|
+
if (!b4a.equals(adminPublicKey, requesterPublicKey)) {
|
|
5234
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "System admin and node public keys do not match.", node.from.key)
|
|
5235
|
+
return Status.FAILURE;
|
|
5236
|
+
}
|
|
5237
|
+
|
|
5238
|
+
const encodedConsensusConfig = safeEncodeConsensusConfig(op.cco.cc);
|
|
5239
|
+
|
|
5240
|
+
if (encodedConsensusConfig.length === 0) {
|
|
5241
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Failed to encode consensus config.", node.from.key);
|
|
5242
|
+
return Status.FAILURE;
|
|
5243
|
+
}
|
|
5244
|
+
|
|
5245
|
+
if (!this.#validateConsensusConfigApply(op.cco.cc)) {
|
|
5246
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Consensus config validation failed.", node.from.key);
|
|
5247
|
+
return Status.FAILURE;
|
|
5248
|
+
}
|
|
5249
|
+
|
|
5250
|
+
// verify requester signature
|
|
5251
|
+
const message = createMessage(
|
|
5252
|
+
this.#config.networkId,
|
|
5253
|
+
op.cco.txv,
|
|
5254
|
+
encodedConsensusConfig,
|
|
5255
|
+
op.cco.in,
|
|
5256
|
+
OperationType.SET_GENESIS_EPOCH
|
|
5257
|
+
);
|
|
5258
|
+
|
|
5259
|
+
if (message.length === 0) {
|
|
5260
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Invalid requester message.", node.from.key)
|
|
5261
|
+
return Status.FAILURE;
|
|
5262
|
+
}
|
|
5263
|
+
|
|
5264
|
+
const hash = await tracCryptoApi.hash.blake3Safe(message);
|
|
5265
|
+
if (!b4a.equals(hash, op.cco.tx)) {
|
|
5266
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Message hash does not match the tx_hash.", node.from.key)
|
|
5267
|
+
return Status.FAILURE;
|
|
5268
|
+
}
|
|
5269
|
+
|
|
5270
|
+
// verify signature
|
|
5271
|
+
const isMessageVerified = tracCryptoApi.signature.verify(op.cco.is, op.cco.tx, adminPublicKey)
|
|
5272
|
+
const txHashHexString = op.cco.tx.toString('hex');
|
|
5273
|
+
|
|
5274
|
+
if (!isMessageVerified) {
|
|
5275
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Failed to verify message signature.", node.from.key)
|
|
5276
|
+
return Status.FAILURE;
|
|
5277
|
+
}
|
|
5278
|
+
|
|
5279
|
+
// verify tx validity - prevent deferred execution attack
|
|
5280
|
+
const indexersSequenceState = await this.#getIndexerSequenceStateApply(base);
|
|
5281
|
+
if (indexersSequenceState === null) {
|
|
5282
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Indexer sequence state is invalid.", node.from.key)
|
|
5283
|
+
return Status.FAILURE;
|
|
5284
|
+
}
|
|
5285
|
+
|
|
5286
|
+
if (!b4a.equals(op.cco.txv, indexersSequenceState)) {
|
|
5287
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Transaction was not executed.", node.from.key)
|
|
5288
|
+
return Status.FAILURE;
|
|
5289
|
+
}
|
|
5290
|
+
|
|
5291
|
+
// anti-replay attack
|
|
5292
|
+
const opEntry = await this.#getEntryApply(txHashHexString, batch);
|
|
5293
|
+
if (opEntry !== null) {
|
|
5294
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Operation has already been applied.", node.from.key)
|
|
5295
|
+
return Status.IGNORE;
|
|
5296
|
+
}
|
|
5297
|
+
|
|
5298
|
+
// check if CurrentEpoch have been initialized if yes - failure
|
|
5299
|
+
const currentEpoch = await this.#getEntryApply(EntryType.EPOCH_CURRENT, batch);
|
|
5300
|
+
if (currentEpoch !== null) {
|
|
5301
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Current epoch is set. Cannot set a new genesis epoch", node.from.key)
|
|
5302
|
+
return Status.IGNORE;
|
|
5303
|
+
}
|
|
5304
|
+
|
|
5305
|
+
// check if genesis epoch is initialized. If yes - failure
|
|
5306
|
+
const epochZero = EntryType.EPOCH + "0";
|
|
5307
|
+
const genesisEpochHash = await this.#getEntryApply(epochZero , batch);
|
|
5308
|
+
if (genesisEpochHash !== null) {
|
|
5309
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Genesis epoch is set. Cannot set a new one", node.from.key)
|
|
5310
|
+
return Status.IGNORE;
|
|
5311
|
+
}
|
|
5312
|
+
|
|
5313
|
+
const currentConsensusConfigIndex = await this.#getEntryApply(
|
|
5314
|
+
EntryType.CONSENSUS_CONFIG_CURRENT,
|
|
5315
|
+
batch
|
|
5316
|
+
);
|
|
5317
|
+
|
|
5318
|
+
const genesisConsensusConfigKey = EntryType.CONSENSUS_CONFIG_RECORD + 0;
|
|
5319
|
+
|
|
5320
|
+
const genesisConsensusConfig = await this.#getEntryApply(
|
|
5321
|
+
genesisConsensusConfigKey,
|
|
5322
|
+
batch
|
|
5323
|
+
);
|
|
5324
|
+
|
|
5325
|
+
// Check if currently genesis config exists
|
|
5326
|
+
if (currentConsensusConfigIndex !== null || genesisConsensusConfig !== null) {
|
|
5327
|
+
this.#safeLogApply(
|
|
5328
|
+
OperationType.SET_GENESIS_EPOCH,
|
|
5329
|
+
"Genesis consensus config is set. Cannot set a new genesis epoch",
|
|
5330
|
+
node.from.key
|
|
5331
|
+
);
|
|
5332
|
+
return Status.IGNORE;
|
|
5333
|
+
}
|
|
5334
|
+
|
|
5335
|
+
|
|
5336
|
+
const genesisEpoch = await createGenesisEpochProof(
|
|
5337
|
+
requesterAddressString,
|
|
5338
|
+
encodedConsensusConfig,
|
|
5339
|
+
this.#config
|
|
5340
|
+
);
|
|
5341
|
+
|
|
5342
|
+
if (!b4a.isBuffer(genesisEpoch) || genesisEpoch.length === 0) {
|
|
5343
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Could not initialize genesis epoch", node.from.key)
|
|
5344
|
+
return Status.FAILURE;
|
|
5345
|
+
}
|
|
5346
|
+
|
|
5347
|
+
const epochProofHash = await tracCryptoApi.hash.blake3Safe(genesisEpoch);
|
|
5348
|
+
if (!isBufferValid(epochProofHash, HASH_BYTE_LENGTH)) {
|
|
5349
|
+
this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Failed to hash genesis epoch proof.", node.from.key)
|
|
5350
|
+
return Status.FAILURE;
|
|
5351
|
+
}
|
|
5352
|
+
|
|
5353
|
+
// initialize CurrentEpoch field
|
|
5354
|
+
const zeroAsUint64Buffer = b4a.alloc(8, 0);
|
|
5355
|
+
await batch.put(
|
|
5356
|
+
EntryType.EPOCH_CURRENT,
|
|
5357
|
+
zeroAsUint64Buffer
|
|
5358
|
+
);
|
|
5359
|
+
|
|
5360
|
+
// initialize Epoch Field
|
|
5361
|
+
await batch.put(
|
|
5362
|
+
epochZero,
|
|
5363
|
+
epochProofHash
|
|
5364
|
+
);
|
|
5365
|
+
|
|
5366
|
+
// initialize EpochHash Field
|
|
5367
|
+
const epochProofHashString = epochProofHash.toString('hex');
|
|
5368
|
+
const epochHashLedgerEntry = EntryType.EPOCH_HASH + epochProofHashString;
|
|
5369
|
+
await batch.put(
|
|
5370
|
+
epochHashLedgerEntry,
|
|
5371
|
+
genesisEpoch
|
|
5372
|
+
);
|
|
5373
|
+
|
|
5374
|
+
// Record 0 preserves the initial config, including the genesis format version.
|
|
5375
|
+
await batch.put(
|
|
5376
|
+
EntryType.CONSENSUS_CONFIG_CURRENT,
|
|
5377
|
+
safeWriteUInt32BE(0)
|
|
5378
|
+
);
|
|
5379
|
+
await batch.put(
|
|
5380
|
+
genesisConsensusConfigKey,
|
|
5381
|
+
encodedConsensusConfig
|
|
5382
|
+
);
|
|
5383
|
+
|
|
5384
|
+
// Put txHashHexString into the state to avoid replay attack
|
|
5385
|
+
await batch.put(txHashHexString, node.value);
|
|
5386
|
+
|
|
5387
|
+
if (this.#config.enableTxApplyLogs) {
|
|
5388
|
+
console.info(`Genesis Epoch initialized addr:wk:tx - ${requesterAddressString}:${decodedAdminEntry.wk.toString('hex')}:${txHashHexString}`);
|
|
5389
|
+
}
|
|
5390
|
+
|
|
5391
|
+
this.#emitEvent(CustomEventType.GENESIS_EPOCH_CREATED, { epoch: 0n, proposerAddress: requesterAddressString });
|
|
5392
|
+
return Status.SUCCESS;
|
|
5393
|
+
}
|
|
5394
|
+
|
|
5395
|
+
async #handleApplySetConsensusConfig(op, _view, base, node, batch, postApplyEvents) {
|
|
5396
|
+
if (!this.#stateValidationSchema.validateConsensusControlOperation(op)) {
|
|
5397
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Contract schema validation failed.", node.from.key)
|
|
5398
|
+
return Status.FAILURE;
|
|
5399
|
+
}
|
|
5400
|
+
|
|
5401
|
+
const requesterAddressString = addressUtils.bufferToAddress(op.address, this.#config.addressPrefix);
|
|
5402
|
+
if (requesterAddressString === null) {
|
|
5403
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Requester address is invalid.", node.from.key)
|
|
5404
|
+
return Status.FAILURE;
|
|
5405
|
+
}
|
|
5406
|
+
|
|
5407
|
+
const requesterPublicKey = tracCryptoApi.address.decodeSafe(requesterAddressString);
|
|
5408
|
+
if (b4a.equals(requesterPublicKey, NULL_BUFFER)) {
|
|
5409
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Failed to decode requester public key.", node.from.key)
|
|
5410
|
+
return Status.FAILURE;
|
|
5411
|
+
}
|
|
5412
|
+
|
|
5413
|
+
const adminEntry = await this.#getEntryApply(EntryType.ADMIN, batch);
|
|
5414
|
+
if (adminEntry === null) {
|
|
5415
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Invalid admin entry.", node.from.key)
|
|
5416
|
+
return Status.FAILURE;
|
|
5417
|
+
}
|
|
5418
|
+
|
|
5419
|
+
const decodedAdminEntry = adminEntryUtils.decode(adminEntry, this.#config.addressPrefix);
|
|
5420
|
+
if (decodedAdminEntry === null) {
|
|
5421
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Failed to decode admin entry.", node.from.key)
|
|
5422
|
+
return Status.FAILURE;
|
|
5423
|
+
}
|
|
5424
|
+
|
|
5425
|
+
if (!this.#isAdminApply(decodedAdminEntry, node)) {
|
|
5426
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Node is not allowed to perform this operation. (ADMIN ONLY)", node.from.key)
|
|
5427
|
+
return Status.FAILURE;
|
|
5428
|
+
}
|
|
5429
|
+
|
|
5430
|
+
const adminPublicKey = tracCryptoApi.address.decodeSafe(decodedAdminEntry.address);
|
|
5431
|
+
if (b4a.equals(adminPublicKey, NULL_BUFFER)) {
|
|
5432
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Failed to decode admin public key.", node.from.key)
|
|
5433
|
+
return Status.FAILURE;
|
|
5434
|
+
}
|
|
5435
|
+
|
|
5436
|
+
if (!b4a.equals(adminPublicKey, requesterPublicKey)) {
|
|
5437
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "System admin and node public keys do not match.", node.from.key)
|
|
5438
|
+
return Status.FAILURE;
|
|
5439
|
+
}
|
|
5440
|
+
|
|
5441
|
+
const encodedConsensusConfig = safeEncodeConsensusConfig(op.cco.cc);
|
|
5442
|
+
|
|
5443
|
+
if (encodedConsensusConfig.length === 0) {
|
|
5444
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Failed to encode consensus config.", node.from.key);
|
|
5445
|
+
return Status.FAILURE;
|
|
5446
|
+
}
|
|
5447
|
+
|
|
5448
|
+
if (!this.#validateConsensusConfigApply(op.cco.cc)) {
|
|
5449
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Consensus config validation failed.", node.from.key);
|
|
5450
|
+
return Status.FAILURE;
|
|
5451
|
+
}
|
|
5452
|
+
|
|
5453
|
+
const message = createMessage(
|
|
5454
|
+
this.#config.networkId,
|
|
5455
|
+
op.cco.txv,
|
|
5456
|
+
encodedConsensusConfig,
|
|
5457
|
+
op.cco.in,
|
|
5458
|
+
OperationType.SET_CONSENSUS_CONFIG
|
|
5459
|
+
);
|
|
5460
|
+
|
|
5461
|
+
if (message.length === 0) {
|
|
5462
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Invalid requester message.", node.from.key)
|
|
5463
|
+
return Status.FAILURE;
|
|
5464
|
+
}
|
|
5465
|
+
|
|
5466
|
+
const hash = await tracCryptoApi.hash.blake3Safe(message);
|
|
5467
|
+
if (!b4a.equals(hash, op.cco.tx)) {
|
|
5468
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Message hash does not match the tx_hash.", node.from.key)
|
|
5469
|
+
return Status.FAILURE;
|
|
5470
|
+
}
|
|
5471
|
+
|
|
5472
|
+
const isMessageVerified = tracCryptoApi.signature.verify(op.cco.is, op.cco.tx, adminPublicKey);
|
|
5473
|
+
if (!isMessageVerified) {
|
|
5474
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Failed to verify message signature.", node.from.key)
|
|
5475
|
+
return Status.FAILURE;
|
|
5476
|
+
}
|
|
5477
|
+
|
|
5478
|
+
const indexersSequenceState = await this.#getIndexerSequenceStateApply(base);
|
|
5479
|
+
if (indexersSequenceState === null) {
|
|
5480
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Indexer sequence state is invalid.", node.from.key)
|
|
5481
|
+
return Status.FAILURE;
|
|
5482
|
+
}
|
|
5483
|
+
|
|
5484
|
+
if (!b4a.equals(op.cco.txv, indexersSequenceState)) {
|
|
5485
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Transaction was not executed.", node.from.key)
|
|
5486
|
+
return Status.FAILURE;
|
|
5487
|
+
}
|
|
5488
|
+
|
|
5489
|
+
const txHashHexString = op.cco.tx.toString('hex');
|
|
5490
|
+
const opEntry = await this.#getEntryApply(txHashHexString, batch);
|
|
5491
|
+
if (opEntry !== null) {
|
|
5492
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Operation has already been applied.", node.from.key)
|
|
5493
|
+
return Status.IGNORE;
|
|
5494
|
+
}
|
|
5495
|
+
|
|
5496
|
+
const currentConsensusConfigIndexBuffer = await this.#getEntryApply(EntryType.CONSENSUS_CONFIG_CURRENT, batch);
|
|
5497
|
+
if (currentConsensusConfigIndexBuffer === null) {
|
|
5498
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Initial consensus config has not been initialized yet", node.from.key)
|
|
5499
|
+
return Status.IGNORE;
|
|
5500
|
+
}
|
|
5501
|
+
|
|
5502
|
+
if (currentConsensusConfigIndexBuffer.length !== CONSENSUS_CONFIG_INDEX_SIZE) {
|
|
5503
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Consensus config index is malformed or corrupted.", node.from.key)
|
|
5504
|
+
return Status.FAILURE;
|
|
5505
|
+
}
|
|
5506
|
+
|
|
5507
|
+
const currentConsensusConfigIndex = safeReadUint32BE(currentConsensusConfigIndexBuffer);
|
|
5508
|
+
if (currentConsensusConfigIndex === null) {
|
|
5509
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG,"Failed to read current consensus config index from buffer", node.from.key)
|
|
5510
|
+
return Status.FAILURE;
|
|
5511
|
+
}
|
|
5512
|
+
if (currentConsensusConfigIndex === UINT32_MAX) {
|
|
5513
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Consensus config index overflow.", node.from.key)
|
|
5514
|
+
return Status.FAILURE;
|
|
5515
|
+
}
|
|
5516
|
+
|
|
5517
|
+
const currentConsensusConfigBuffer = await this.#getEntryApply(
|
|
5518
|
+
EntryType.CONSENSUS_CONFIG_RECORD + currentConsensusConfigIndex,
|
|
5519
|
+
batch
|
|
5520
|
+
);
|
|
5521
|
+
const currentConsensusConfig = safeDecodeConsensusConfig(currentConsensusConfigBuffer);
|
|
5522
|
+
if (currentConsensusConfig === null) {
|
|
5523
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Failed to decode current consensus config.", node.from.key)
|
|
5524
|
+
return Status.FAILURE;
|
|
5525
|
+
}
|
|
5526
|
+
|
|
5527
|
+
const currentSchemaVersion = safeReadUint8(currentConsensusConfig.sv);
|
|
5528
|
+
const nextSchemaVersion = safeReadUint8(op.cco.cc.sv);
|
|
5529
|
+
if (currentSchemaVersion === null || currentSchemaVersion === 0) {
|
|
5530
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Current consensus config schema version is invalid.", node.from.key)
|
|
5531
|
+
return Status.FAILURE;
|
|
5532
|
+
}
|
|
5533
|
+
|
|
5534
|
+
// Same-version parameter changes are allowed; returning to an older consensus is not.
|
|
5535
|
+
if (nextSchemaVersion < currentSchemaVersion) {
|
|
5536
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Consensus config schema version cannot decrease.", node.from.key)
|
|
5537
|
+
return Status.FAILURE;
|
|
5538
|
+
}
|
|
5539
|
+
|
|
5540
|
+
const nextConsensusConfigIndex = currentConsensusConfigIndex + 1;
|
|
5541
|
+
const nextConsensusConfigKey = EntryType.CONSENSUS_CONFIG_RECORD + nextConsensusConfigIndex;
|
|
5542
|
+
const nextConsensusConfigIndexBuffer = safeWriteUInt32BE(nextConsensusConfigIndex);
|
|
5543
|
+
|
|
5544
|
+
if (nextConsensusConfigIndexBuffer.length === 0) {
|
|
5545
|
+
this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Failed to encode next consensus config index.", node.from.key);
|
|
5546
|
+
return Status.FAILURE;
|
|
5547
|
+
}
|
|
5548
|
+
|
|
5549
|
+
await batch.put(EntryType.CONSENSUS_CONFIG_CURRENT, nextConsensusConfigIndexBuffer);
|
|
5550
|
+
await batch.put(nextConsensusConfigKey, encodedConsensusConfig);
|
|
5551
|
+
await batch.put(txHashHexString, node.value);
|
|
5552
|
+
|
|
5553
|
+
if (this.#config.enableTxApplyLogs) {
|
|
5554
|
+
console.info(`Consensus config updated addr:wk:tx - ${requesterAddressString}:${decodedAdminEntry.wk.toString('hex')}:${txHashHexString}`);
|
|
5555
|
+
}
|
|
5556
|
+
|
|
5557
|
+
postApplyEvents.push({
|
|
5558
|
+
type: CustomEventType.CONSENSUS_CONFIG_CHANGED,
|
|
5559
|
+
});
|
|
5560
|
+
|
|
5561
|
+
return Status.SUCCESS;
|
|
5562
|
+
}
|
|
5563
|
+
|
|
3943
5564
|
}
|
|
3944
5565
|
|
|
3945
5566
|
export default State;
|