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.
Files changed (137) hide show
  1. package/SECURITY.md +2 -2
  2. package/cli/commandHandler.js +317 -20
  3. package/cli/handlers.js +9 -1
  4. package/cli/interactive.js +4 -3
  5. package/cli/tokenSupplyAudit.js +90 -0
  6. package/msb.mjs +1 -0
  7. package/package.json +15 -6
  8. package/rpc/create_server.js +12 -3
  9. package/rpc/handlers.js +103 -0
  10. package/rpc/routes/v1.js +8 -0
  11. package/src/codecs/apply/applyOperationCodec.js +334 -0
  12. package/src/codecs/apply/applyOperations.generated.cjs +6380 -0
  13. package/src/codecs/consensus/v1/consensusV1.generated.cjs +3508 -0
  14. package/src/codecs/consensus/v1/consensusV1OperationCodec.js +161 -0
  15. package/src/codecs/consensus/v1/vdfConfigCodec.js +83 -0
  16. package/src/{utils/protobuf → codecs/network/v1}/networkV1.generated.cjs +664 -393
  17. package/src/codecs/network/v1/networkV1OperationCodec.js +25 -0
  18. package/src/codecs/protobufRuntime.cjs +95 -0
  19. package/src/codecs/state/escrowEntryCodec.js +45 -0
  20. package/src/codecs/state/state.generated.cjs +659 -0
  21. package/src/config/args.js +0 -1
  22. package/src/config/config.js +46 -7
  23. package/src/config/env.js +33 -6
  24. package/src/core/consensus/epochAppenderSelection/EpochAppenderSelectionError.js +55 -0
  25. package/src/core/consensus/epochAppenderSelection/EpochAppenderSelectionOperations.js +118 -0
  26. package/src/core/consensus/epochAppenderSelection/EpochAppenderSelector.js +1042 -0
  27. package/src/core/consensus/epochAppenderSelection/createEpochAppenderSelector.js +23 -0
  28. package/src/core/consensus/epochAppenderSelection/epochAppenderSelectionLedgerReader.js +274 -0
  29. package/src/core/consensus/epochAppenderSelection/epochAppenderSelectionState.js +488 -0
  30. package/src/core/consensus/epochAppenderSelection/epochAppenderSelectionStorage.js +93 -0
  31. package/src/core/consensus/quorum.js +11 -0
  32. package/src/core/consensus/services/EpochCoordinationRound.js +839 -0
  33. package/src/core/consensus/services/EpochCoordinatorOperations.js +211 -0
  34. package/src/core/consensus/services/EpochCoordinatorService.js +335 -0
  35. package/src/core/consensus/services/EpochRoundListeners.js +116 -0
  36. package/src/core/consensus/services/EpochStateMachine.js +141 -0
  37. package/src/core/consensus/services/VDFBare.js +291 -0
  38. package/src/core/consensus/services/VDFNode.js +111 -0
  39. package/src/core/consensus/services/VDFProgressWatchdog.js +79 -0
  40. package/src/core/consensus/services/VDFService.js +87 -0
  41. package/src/core/consensus/services/VDFServiceManager.js +147 -0
  42. package/src/core/consensus/services/createVDFService.js +16 -0
  43. package/src/core/consensus/services/vdf-guardian.js +32 -0
  44. package/src/core/consensus/services/vdf-worker.js +90 -0
  45. package/src/core/network/Network.js +327 -238
  46. package/src/core/network/protocols/consensus/IndexerMessages.js +72 -0
  47. package/src/core/network/protocols/consensus/v1/ConsensusV1ConnectionPolicy.js +57 -0
  48. package/src/core/network/protocols/consensus/v1/ConsensusV1MessageRouter.js +147 -0
  49. package/src/core/network/protocols/consensus/v1/ConsensusV1Protocol.js +72 -0
  50. package/src/core/network/protocols/consensus/v1/IndexerPendingRequestService.js +108 -0
  51. package/src/core/network/protocols/consensus/v1/V1ConsensusProtocolError.js +29 -0
  52. package/src/core/network/protocols/consensus/v1/handlers/V1EpochAppendOperationHandler.js +68 -0
  53. package/src/core/network/protocols/consensus/v1/handlers/V1EpochProofProposalOperationHandler.js +226 -0
  54. package/src/core/network/protocols/consensus/v1/validators/ConsensusValidationSchema.js +192 -0
  55. package/src/core/network/protocols/consensus/v1/validators/V1BaseConsensusOperation.js +428 -0
  56. package/src/core/network/protocols/consensus/v1/validators/V1EpochAppendReply.js +47 -0
  57. package/src/core/network/protocols/consensus/v1/validators/V1EpochAppendRequest.js +17 -0
  58. package/src/core/network/protocols/consensus/v1/validators/V1EpochProofProposalApproval.js +116 -0
  59. package/src/core/network/protocols/consensus/v1/validators/V1EpochProofProposalRequest.js +152 -0
  60. package/src/core/network/protocols/consensus/v1/validators/consensusValidationSchemaHelpers.js +145 -0
  61. package/src/core/network/protocols/shared/ConnectionOperationHandler.js +27 -0
  62. package/src/core/network/protocols/{NetworkMessages.js → validators/ValidatorMessages.js} +24 -14
  63. package/src/core/network/protocols/{ProtocolSession.js → validators/ValidatorProtocolSession.js} +22 -25
  64. package/src/core/network/protocols/{legacy/NetworkMessageRouter.js → validators/legacy/ValidatorLegacyMessageRouter.js} +6 -6
  65. package/src/core/network/protocols/{LegacyProtocol.js → validators/legacy/ValidatorLegacyProtocol.js} +9 -7
  66. package/src/core/network/protocols/{legacy → validators/legacy}/handlers/BaseStateOperationHandler.js +1 -1
  67. package/src/core/network/protocols/{legacy → validators/legacy}/handlers/LegacyGetRequestHandler.js +5 -5
  68. package/src/core/network/protocols/{legacy → validators/legacy}/handlers/LegacyRoleOperationHandler.js +5 -5
  69. package/src/core/network/protocols/{legacy → validators/legacy}/handlers/LegacySubnetworkOperationHandler.js +5 -5
  70. package/src/core/network/protocols/{legacy → validators/legacy}/handlers/LegacyTransferOperationHandler.js +5 -5
  71. package/src/core/network/protocols/{ProtocolInterface.js → validators/shared/ValidatorProtocolInterface.js} +5 -5
  72. package/src/core/network/protocols/{shared → validators/shared}/validators/PartialBootstrapDeploymentValidator.js +1 -1
  73. package/src/core/network/protocols/validators/shared/validators/PartialHtlcValidator.js +325 -0
  74. package/src/core/network/protocols/{shared → validators/shared}/validators/PartialOperationValidator.js +44 -21
  75. package/src/core/network/protocols/{shared → validators/shared}/validators/PartialRoleAccessValidator.js +3 -3
  76. package/src/core/network/protocols/{shared → validators/shared}/validators/PartialTransactionValidator.js +3 -3
  77. package/src/core/network/protocols/{shared → validators/shared}/validators/PartialTransferValidator.js +4 -4
  78. package/src/core/network/protocols/{v1 → validators/v1}/V1ProtocolError.js +1 -1
  79. package/src/core/network/{services/PendingRequestService.js → protocols/validators/v1/ValidatorPendingRequestService.js} +19 -82
  80. package/src/core/network/protocols/{v1/NetworkMessageRouter.js → validators/v1/ValidatorV1MessageRouter.js} +9 -9
  81. package/src/core/network/protocols/{V1Protocol.js → validators/v1/ValidatorV1Protocol.js} +10 -8
  82. package/src/core/network/protocols/{connectionPolicies.js → validators/v1/connectionPolicies.js} +1 -1
  83. package/src/core/network/protocols/{v1 → validators/v1}/handlers/V1BaseOperationHandler.js +6 -23
  84. package/src/core/network/protocols/{v1 → validators/v1}/handlers/V1BroadcastTransactionOperationHandler.js +14 -9
  85. package/src/core/network/protocols/{v1 → validators/v1}/handlers/V1LivenessOperationHandler.js +5 -4
  86. package/src/core/network/protocols/{v1 → validators/v1}/handlers/broadcastTransaction/BroadcastTransactionErrorMapper.js +3 -3
  87. package/src/core/network/protocols/{v1 → validators/v1}/handlers/broadcastTransaction/BroadcastTransactionOperationStrategies.js +44 -1
  88. package/src/core/network/protocols/{v1 → validators/v1}/handlers/broadcastTransaction/BroadcastTransactionPayloadUtils.js +1 -1
  89. package/src/core/network/protocols/{v1 → validators/v1}/validators/V1BaseOperation.js +19 -15
  90. package/src/core/network/protocols/{v1 → validators/v1}/validators/V1BroadcastTransactionRequest.js +2 -2
  91. package/src/core/network/protocols/{v1 → validators/v1}/validators/V1BroadcastTransactionResponse.js +34 -18
  92. package/src/core/network/protocols/{v1 → validators/v1}/validators/V1ValidationSchema.js +3 -3
  93. package/src/core/network/services/IndexerConnectionManager.js +88 -0
  94. package/src/core/network/services/TransactionPoolService.js +11 -18
  95. package/src/core/network/services/TransactionRateLimiterService.js +1 -1
  96. package/src/core/network/services/ValidatorConnectionManager.js +163 -0
  97. package/src/core/network/services/{MessageOrchestrator.js → ValidatorMessageOrchestrator.js} +34 -42
  98. package/src/core/network/services/ValidatorObserverService.js +21 -36
  99. package/src/core/network/shared/BasePendingRequestService.js +67 -0
  100. package/src/core/network/shared/PeerConnectionManager.js +185 -0
  101. package/src/core/state/ConfirmationWatch.js +82 -0
  102. package/src/core/state/State.js +1794 -173
  103. package/src/core/state/StateEventQueue.js +69 -0
  104. package/src/core/state/utils/address.js +10 -6
  105. package/src/core/state/utils/epochProof.js +109 -0
  106. package/src/core/state/utils/escrowEntry.js +178 -0
  107. package/src/core/state/validators/StateValidationSchema.js +757 -0
  108. package/src/core/state/validators/stateValidationRules.js +312 -0
  109. package/src/index.js +255 -65
  110. package/src/messages/consensus/v1/ConsensusMessageBuilder.js +368 -0
  111. package/src/messages/consensus/v1/ConsensusMessageDirector.js +142 -0
  112. package/src/messages/consensus/v1/consensusMessageFactory.js +12 -0
  113. package/src/messages/consensus/v1/epochAppendMessage.js +58 -0
  114. package/src/messages/network/v1/NetworkMessageBuilder.js +15 -7
  115. package/src/messages/state/ApplyStateMessageBuilder.js +499 -13
  116. package/src/messages/state/ApplyStateMessageDirector.js +261 -2
  117. package/src/utils/StateMachine.js +125 -0
  118. package/src/utils/applyOperations.js +45 -2
  119. package/src/utils/buffer.js +239 -59
  120. package/src/utils/consensusConfig.js +76 -0
  121. package/src/utils/constants.js +174 -73
  122. package/src/utils/helpers.js +0 -25
  123. package/src/utils/htlcLock.js +122 -0
  124. package/src/utils/logger.js +10 -4
  125. package/src/utils/migrationUtils.js +2 -1
  126. package/src/utils/normalizers.js +113 -0
  127. package/src/utils/scheduler/SchedulableService.js +107 -0
  128. package/src/utils/{Scheduler.js → scheduler/Scheduler.js} +56 -17
  129. package/src/core/network/services/ConnectionManager.js +0 -406
  130. package/src/utils/check.js +0 -525
  131. package/src/utils/protobuf/applyOperations.cjs +0 -1373
  132. package/src/utils/protobuf/operationHelpers.js +0 -81
  133. /package/src/core/network/protocols/{legacy → validators/legacy}/handlers/LegacyResponseHandler.js +0 -0
  134. /package/src/core/network/protocols/{legacy → validators/legacy}/validators/ValidatorResponse.js +0 -0
  135. /package/src/core/network/protocols/{legacy → validators/legacy}/validators/base/BaseResponse.js +0 -0
  136. /package/src/core/network/protocols/{v1 → validators/v1}/validators/V1LivenessRequest.js +0 -0
  137. /package/src/core/network/protocols/{v1 → validators/v1}/validators/V1LivenessResponse.js +0 -0
@@ -5,43 +5,87 @@ import b4a from 'b4a';
5
5
  import {
6
6
  ACK_INTERVAL,
7
7
  ADMIN_INITIAL_BALANCE,
8
- EntryType,
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
- BATCH_SIZE,
14
- ADMIN_INITIAL_STAKED_BALANCE,
21
+ OperationType,
15
22
  TRAC_NAMESPACE,
16
- CustomEventType
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 { isHexString, sleep } from '../../utils/helpers.js';
28
+ import {isHexString, isTransactionRecordPut, sleep} from '../../utils/helpers.js';
19
29
  import tracCryptoApi from 'trac-crypto-api';
20
- import Check from '../../utils/check.js';
21
- import { safeDecodeApplyOperation } from '../../utils/protobuf/operationHelpers.js';
22
- import { createMessage, ZERO_WK, NULL_BUFFER } from '../../utils/buffer.js';
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, NODE_ENTRY_SIZE } from './utils/nodeEntry.js';
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 { deepCopyBuffer } from '../../utils/buffer.js';
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.check = new Check(config);
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
- async getSigned(key) {
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
- const indexerListHasWk = Object.values(this.#base.system.indexers)
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, 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
- if (startSignedLength === endSignedLength) return [];
398
-
399
- const currentSignedLength = this.getSignedLength();
400
- const signedLength2End = Math.min(currentSignedLength, endSignedLength);
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 historyStream = this.#base.view.createHistoryStream({
410
- gte: startSeq,
411
- lte: endSeq
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
- let hashes = [];
422
- for await (const entry of historyStream) {
423
- if (filters(entry)) {
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
- return hashes;
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
- let invalidOperations = 0;
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
- for (const node of nodes) {
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
- if (b4a.byteLength(node.value) > transactionUtils.MAXIMUM_OPERATION_PAYLOAD_SIZE) {
465
- this.#safeLogApply("Node payload exceeds the maximum operation payload size.", node.from.key)
466
- invalidOperations++;
467
- continue;
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
- const op = safeDecodeApplyOperation(node.value);
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
- if (!op) {
473
- this.#safeLogApply("Failed to decode operation.", node.from.key)
474
- invalidOperations++;
475
- continue;
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
- const handler = this.#getApplyOperationHandler(op.type);
479
-
480
- if (handler) {
481
- const result = await handler(op, view, base, node, batch);
482
- if (result === Status.FAILURE) {
483
- invalidOperations++;
484
- } else if (result === Status.IGNORE) {
485
- continue;
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 (invalidOperations > 0) {
496
- await this.#validatorPenaltyApply(batchInvoker, batch, base, invalidOperations);
497
- this.#safeLogApply(`Applied with ${invalidOperations} invalid operations.`)
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.flush();
501
- await batch.close();
502
- }
1458
+ await batch.put(recipientAddress, updatedRecipientEntry);
1459
+ await batch.put(escrowKey, settlement.refundedEscrowEntry);
1460
+ await batch.put(refundTxHashHex, node.value);
503
1461
 
504
- #getApplyOperationHandler(type) {
505
- const handlers = {
506
- [OperationType.BALANCE_INITIALIZATION]: this.#handleApplyInitializeBalanceOperation.bind(this),
507
- [OperationType.DISABLE_INITIALIZATION]: this.#handleApplyDisableBalanceInitializationOperation.bind(this),
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.check.validateBalanceInitialization(op)) {
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 isMessageVerifed = tracCryptoApi.signature.verify(op.bio.is, hash, adminPublicKey);
621
- if (!isMessageVerifed) {
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.check.validateCoreAdminOperation(op)) {
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 isMessageVerifed = tracCryptoApi.signature.verify(op.cao.is, hash, adminPublicKey);
743
- if (!isMessageVerifed) {
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, 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.check.validateCoreAdminOperation(op)) {
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 isMessageVerifed = tracCryptoApi.signature.verify(op.cao.is, op.cao.tx, adminPublicKey)
1772
+ const isMessageVerified = tracCryptoApi.signature.verify(op.cao.is, op.cao.tx, adminPublicKey)
828
1773
  const txHashHexString = op.cao.tx.toString('hex');
829
- if (!isMessageVerifed) {
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, 0));
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.check.validateRoleAccessOperation(op)) {
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.check.validateAdminControlOperation(op)) {
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.check.validateRoleAccessOperation(op)) {
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.check.validateRoleAccessOperation(op)) {
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.check.validateAdminControlOperation(op)) {
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 isMessageVerifed = tracCryptoApi.signature.verify(op.aco.is, hash, adminPublicKey);
3016
+ const isMessageVerified = tracCryptoApi.signature.verify(op.aco.is, hash, adminPublicKey);
2072
3017
  const txHashHexString = hash.toString('hex');
2073
- if (!isMessageVerifed) {
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.check.validateAdminControlOperation(op)) {
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 isMessageVerifed = tracCryptoApi.signature.verify(op.aco.is, hash, adminPublicKey);
3231
+ const isMessageVerified = tracCryptoApi.signature.verify(op.aco.is, hash, adminPublicKey);
2287
3232
  const txHashHexString = hash.toString('hex');
2288
- if (!isMessageVerifed) {
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.check.validateAdminControlOperation(op)) {
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 isMessageVerifed = tracCryptoApi.signature.verify(op.aco.is, regeneratedHash, adminPublicKey);
3437
+ const isMessageVerified = tracCryptoApi.signature.verify(op.aco.is, regeneratedHash, adminPublicKey);
2490
3438
  const txHashHexString = regeneratedHash.toString('hex');
2491
- if (!isMessageVerifed) {
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.check.validateBootstrapDeploymentOperation(op)) {
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.check.validateTransactionOperation(op)) {
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.check.validateTransferOperation(op)) {
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(hashHexString, node.value);
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(`Transfer operation: ${hashHexString} has been appended.`);
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, 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;