trac-msb 0.2.26 → 0.3.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) 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 +41 -7
  23. package/src/config/env.js +33 -9
  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/State.js +1789 -173
  102. package/src/core/state/StateEventQueue.js +69 -0
  103. package/src/core/state/utils/address.js +10 -6
  104. package/src/core/state/utils/epochProof.js +109 -0
  105. package/src/core/state/utils/escrowEntry.js +178 -0
  106. package/src/core/state/validators/StateValidationSchema.js +757 -0
  107. package/src/core/state/validators/stateValidationRules.js +312 -0
  108. package/src/index.js +255 -65
  109. package/src/messages/consensus/v1/ConsensusMessageBuilder.js +368 -0
  110. package/src/messages/consensus/v1/ConsensusMessageDirector.js +142 -0
  111. package/src/messages/consensus/v1/consensusMessageFactory.js +12 -0
  112. package/src/messages/consensus/v1/epochAppendMessage.js +58 -0
  113. package/src/messages/network/v1/NetworkMessageBuilder.js +15 -7
  114. package/src/messages/state/ApplyStateMessageBuilder.js +499 -13
  115. package/src/messages/state/ApplyStateMessageDirector.js +261 -2
  116. package/src/utils/StateMachine.js +125 -0
  117. package/src/utils/applyOperations.js +45 -2
  118. package/src/utils/buffer.js +239 -59
  119. package/src/utils/consensusConfig.js +76 -0
  120. package/src/utils/constants.js +174 -73
  121. package/src/utils/helpers.js +0 -25
  122. package/src/utils/htlcLock.js +122 -0
  123. package/src/utils/logger.js +10 -4
  124. package/src/utils/migrationUtils.js +2 -1
  125. package/src/utils/normalizers.js +113 -0
  126. package/src/utils/scheduler/SchedulableService.js +107 -0
  127. package/src/utils/{Scheduler.js → scheduler/Scheduler.js} +56 -17
  128. package/src/core/network/services/ConnectionManager.js +0 -406
  129. package/src/utils/check.js +0 -525
  130. package/src/utils/protobuf/applyOperations.cjs +0 -1373
  131. package/src/utils/protobuf/operationHelpers.js +0 -81
  132. /package/src/core/network/protocols/{legacy → validators/legacy}/handlers/LegacyResponseHandler.js +0 -0
  133. /package/src/core/network/protocols/{legacy → validators/legacy}/validators/ValidatorResponse.js +0 -0
  134. /package/src/core/network/protocols/{legacy → validators/legacy}/validators/base/BaseResponse.js +0 -0
  135. /package/src/core/network/protocols/{v1 → validators/v1}/validators/V1LivenessRequest.js +0 -0
  136. /package/src/core/network/protocols/{v1 → validators/v1}/validators/V1LivenessResponse.js +0 -0
@@ -5,43 +5,86 @@ 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';
45
88
  import { ConfirmationWatch } from './ConfirmationWatch.js';
46
89
 
47
90
  const OVERSIZED_BATCH_PENALTY_MULTIPLIER = BATCH_SIZE;
@@ -56,7 +99,10 @@ class State extends ReadyResource {
56
99
  #writingKey;
57
100
  #config
58
101
  #wallet
102
+ #stateValidationSchema;
59
103
  #activeWriterCountCache = new Map();
104
+ #stateEventQueue = new StateEventQueue();
105
+ #viewUpdateListener;
60
106
  #confirmationWatch = null;
61
107
 
62
108
  /**
@@ -71,7 +117,8 @@ class State extends ReadyResource {
71
117
  this.#wallet = wallet
72
118
  this.#store = store;
73
119
 
74
- this.check = new Check(config);
120
+ this.#stateValidationSchema = new StateValidationSchema(config);
121
+ this.#viewUpdateListener = this.handleViewUpdate.bind(this);
75
122
  this.#base = new Autobase(this.#store, this.#config.bootstrap, {
76
123
  ackInterval: ACK_INTERVAL,
77
124
  valueEncoding: AUTOBASE_VALUE_ENCODING,
@@ -90,6 +137,10 @@ class State extends ReadyResource {
90
137
  return this.#writingKey;
91
138
  }
92
139
 
140
+ get stateValidationSchema() {
141
+ return this.#stateValidationSchema;
142
+ }
143
+
93
144
  get applyHandler() {
94
145
  return this.#apply.bind(this);
95
146
  }
@@ -98,6 +149,11 @@ class State extends ReadyResource {
98
149
  console.log("State initialization...")
99
150
  await this.#base.ready();
100
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()
101
157
  this.#confirmationWatch = new ConfirmationWatch(this.#base, { timeout: this.#config.confirmationStallTimeout });
102
158
  this.#confirmationWatch.start();
103
159
  }
@@ -105,16 +161,55 @@ class State extends ReadyResource {
105
161
  async _close() {
106
162
  console.log("State: closing gracefully...");
107
163
  this.#confirmationWatch?.stop();
164
+
165
+ this.#base.off('update', this.#viewUpdateListener);
166
+ this.#stateEventQueue.clear();
167
+ this.removeAllListeners();
168
+
108
169
  if (this.#bee !== null) {
109
170
  await this.#bee.close();
110
171
  }
111
172
  await sleep(100);
112
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
+
113
182
  if (this.#base !== null) {
114
183
  await this.#base.close();
115
184
  }
116
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
+ }
117
208
 
209
+ #emitEvent(event, ...args) {
210
+ try {
211
+ this.emit(event, ...args)
212
+ } catch (_ignored) { }
118
213
  }
119
214
 
120
215
  isWritable() {
@@ -125,6 +220,10 @@ class State extends ReadyResource {
125
220
  return this.#base.isIndexer;
126
221
  }
127
222
 
223
+ async indexerCount() {
224
+ return (await this.getIndexersEntry()).length;
225
+ }
226
+
128
227
  getUnsignedLength() {
129
228
  return this.#base.view.core.length;
130
229
  }
@@ -133,6 +232,110 @@ class State extends ReadyResource {
133
232
  return this.#base.view.core.signedLength;
134
233
  }
135
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
+
136
339
  getFee() {
137
340
  return transactionUtils.FEE;
138
341
  }
@@ -154,10 +357,22 @@ class State extends ReadyResource {
154
357
  return false;
155
358
  }
156
359
 
157
- 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 = {}) {
158
373
  const view_session = this.#base.view.checkout(this.#base.view.core.signedLength);
159
374
  try {
160
- const result = await view_session.get(key);
375
+ const result = await view_session.get(key, options);
161
376
  return result ? result.value : null;
162
377
  } finally {
163
378
  await view_session.close();
@@ -197,6 +412,11 @@ class State extends ReadyResource {
197
412
  return !!adminEntry && this.#wallet?.address === adminEntry?.address && b4a.equals(adminEntry?.wk, this.writingKey)
198
413
  }
199
414
 
415
+ async isAdminAddress(targetAddress) {
416
+ const adminEntry = await this.getAdminEntry();
417
+ return (adminEntry?.address === targetAddress);
418
+ }
419
+
200
420
  async isAdminAllowedToValidate() {
201
421
  if (!this.writingKey) return false;
202
422
 
@@ -217,6 +437,23 @@ class State extends ReadyResource {
217
437
  return Object.values(this.#base.system.indexers);
218
438
  }
219
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
+
220
457
  async getActiveWriterCount(excludeAdmin = false) {
221
458
  const cached = this.#activeWriterCountCache.get(excludeAdmin);
222
459
  const systemLength = this.#base.system?.core?.length ?? -1;
@@ -228,6 +465,10 @@ class State extends ReadyResource {
228
465
  const activeAddresses = new Set();
229
466
  const adminAddress = (await this.getAdminEntry())?.address ?? null;
230
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
+
231
472
  for await (const { key, value } of this.#base.system.list()) {
232
473
  if (!key || !value || value.isRemoved) continue;
233
474
 
@@ -250,11 +491,26 @@ class State extends ReadyResource {
250
491
  return count;
251
492
  }
252
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
+
253
510
  async isWkInIndexersEntry(wk) {
254
511
  if (wk === null) return false;
255
- const indexerListHasWk = Object.values(this.#base.system.indexers)
512
+ return Object.values(this.#base.system.indexers)
256
513
  .some(entry => b4a.equals(entry.key, wk));
257
- return indexerListHasWk;
258
514
  }
259
515
 
260
516
  async getWriterLength() {
@@ -368,7 +624,7 @@ class State extends ReadyResource {
368
624
  if (initialization === null) {
369
625
  return false
370
626
  } else {
371
- return b4a.equals(initialization, safeWriteUInt32BE(0, 0))
627
+ return b4a.equals(initialization, safeWriteUInt32BE(0))
372
628
  }
373
629
  }
374
630
 
@@ -395,138 +651,822 @@ class State extends ReadyResource {
395
651
  throw new Error("Params must be non-negative");
396
652
  }
397
653
 
398
- if (startSignedLength > endSignedLength) {
399
- 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;
400
1384
  }
401
1385
 
402
- if (startSignedLength === endSignedLength) return [];
403
-
404
- const currentSignedLength = this.getSignedLength();
405
- const signedLength2End = Math.min(currentSignedLength, endSignedLength);
406
-
407
- const startSeq = startSignedLength;
408
- const endSeq = signedLength2End - 1;
409
-
410
- if (startSeq > endSeq) {
411
- 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;
412
1390
  }
413
1391
 
414
- const historyStream = this.#base.view.createHistoryStream({
415
- gte: startSeq,
416
- lte: endSeq
417
- });
418
-
419
- const filters = (entry) => {
420
- const isPut = entry.type === "put";
421
- const isHex = isHexString(entry.key);
422
- const is64 = entry.key.length === 64;
423
- 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;
424
1397
  }
425
-
426
- let hashes = [];
427
- for await (const entry of historyStream) {
428
- if (filters(entry)) {
429
- hashes.push({ hash: entry.key, confirmed_length: entry.seq });
430
- }
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;
431
1402
  }
432
-
433
- return hashes;
434
- }
435
-
436
- async getRegisteredWriterKey(writingKey) {
437
- const entry = await this.get(EntryType.WRITER_ADDRESS + writingKey);
438
- return entry ? addressUtils.addressToBuffer(entry, this.#config.addressPrefix) : null;
439
- }
440
-
441
- #setupHyperbee(store) {
442
- this.#bee = new Hyperbee(store.get(TRAC_NAMESPACE), {
443
- extension: false,
444
- keyEncoding: HYPERBEE_KEY_ENCODING,
445
- valueEncoding: HYPERBEE_VALUE_ENCODING,
446
- })
447
- return this.#bee;
448
- }
449
-
450
- // ATTENTION: DO NOT USE METHODS ABOVE IN APPLY PART!
451
- ///////////////////////////////APPLY////////////////////////////////////
452
-
453
- async #apply(nodes, view, base) {
454
- const batch = view.batch();
455
- const batchInvoker = nodes[0].from.key;
456
-
457
-
458
- if (nodes.length > BATCH_SIZE) {
459
- await this.#validatorPenaltyApply(batchInvoker, batch, base, OVERSIZED_BATCH_PENALTY_MULTIPLIER);
460
- await batch.flush();
461
- await batch.close();
462
- 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;
463
1406
  }
464
1407
 
465
- 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
+ }
466
1413
 
467
- 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
+ }
468
1418
 
469
- if (b4a.byteLength(node.value) > transactionUtils.MAXIMUM_OPERATION_PAYLOAD_SIZE) {
470
- this.#safeLogApply("Node payload exceeds the maximum operation payload size.", node.from.key)
471
- invalidOperations++;
472
- continue;
473
- };
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
+ }
474
1424
 
475
- 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
+ }
476
1430
 
477
- if (!op) {
478
- this.#safeLogApply("Failed to decode operation.", node.from.key)
479
- invalidOperations++;
480
- 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;
481
1440
  }
482
-
483
- const handler = this.#getApplyOperationHandler(op.type);
484
-
485
- if (handler) {
486
- const result = await handler(op, view, base, node, batch);
487
- if (result === Status.FAILURE) {
488
- invalidOperations++;
489
- } else if (result === Status.IGNORE) {
490
- continue;
491
- } else if (result !== Status.SUCCESS) {
492
- this.#safeLogApply(`Unknown operation status: ${result}`, node.from.key);
493
- invalidOperations++;
494
- }
495
- } else {
496
- this.#safeLogApply(`Unknown operation type: ${op.type}`, node.from.key)
497
- 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;
498
1450
  }
1451
+ updatedRecipientEntry = updatedBalance.update(recipientEntryBuffer);
499
1452
  }
500
- if (invalidOperations > 0) {
501
- await this.#validatorPenaltyApply(batchInvoker, batch, base, invalidOperations);
502
- 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;
503
1456
  }
504
1457
 
505
- await batch.flush();
506
- await batch.close();
507
- }
1458
+ await batch.put(recipientAddress, updatedRecipientEntry);
1459
+ await batch.put(escrowKey, settlement.refundedEscrowEntry);
1460
+ await batch.put(refundTxHashHex, node.value);
508
1461
 
509
- #getApplyOperationHandler(type) {
510
- const handlers = {
511
- [OperationType.BALANCE_INITIALIZATION]: this.#handleApplyInitializeBalanceOperation.bind(this),
512
- [OperationType.DISABLE_INITIALIZATION]: this.#handleApplyDisableBalanceInitializationOperation.bind(this),
513
- [OperationType.ADD_ADMIN]: this.#handleApplyAddAdminOperation.bind(this),
514
- [OperationType.APPEND_WHITELIST]: this.#handleApplyAppendWhitelistOperation.bind(this),
515
- [OperationType.ADD_WRITER]: this.#handleApplyAddWriterOperation.bind(this),
516
- [OperationType.REMOVE_WRITER]: this.#handleApplyRemoveWriterOperation.bind(this),
517
- [OperationType.ADMIN_RECOVERY]: this.#handleApplyAdminRecoveryOperation.bind(this),
518
- [OperationType.ADD_INDEXER]: this.#handleApplyAddIndexerOperation.bind(this),
519
- [OperationType.REMOVE_INDEXER]: this.#handleApplyRemoveIndexerOperation.bind(this),
520
- [OperationType.BAN_VALIDATOR]: this.#handleApplyBanValidatorOperation.bind(this),
521
- [OperationType.BOOTSTRAP_DEPLOYMENT]: this.#handleApplyBootstrapDeploymentOperation.bind(this),
522
- [OperationType.TX]: this.#handleApplyTxOperation.bind(this),
523
- [OperationType.TRANSFER]: this.#handleApplyTransferOperation.bind(this),
524
- };
525
- return handlers[type] || null;
1462
+ if (this.#config.enableTxApplyLogs) {
1463
+ console.info(`HTLC refund operation: ${refundTxHashHex} has been appended.`);
1464
+ }
1465
+ return Status.SUCCESS;
526
1466
  }
527
1467
 
528
1468
  async #handleApplyInitializeBalanceOperation(op, view, base, node, batch) {
529
- if (!this.check.validateBalanceInitialization(op)) {
1469
+ if (!this.#stateValidationSchema.validateBalanceInitialization(op)) {
530
1470
  this.#safeLogApply(OperationType.BALANCE_INITIALIZATION, "Contract schema validation failed.", node.from.key)
531
1471
  return Status.FAILURE;
532
1472
  };
@@ -622,8 +1562,8 @@ class State extends ReadyResource {
622
1562
  };
623
1563
 
624
1564
  // Verify signature
625
- const isMessageVerifed = tracCryptoApi.signature.verify(op.bio.is, hash, adminPublicKey);
626
- if (!isMessageVerifed) {
1565
+ const isMessageVerified = tracCryptoApi.signature.verify(op.bio.is, hash, adminPublicKey);
1566
+ if (!isMessageVerified) {
627
1567
  this.#safeLogApply(OperationType.BALANCE_INITIALIZATION, "Failed to verify message signature.", node.from.key)
628
1568
  return Status.FAILURE;
629
1569
  };
@@ -671,7 +1611,7 @@ class State extends ReadyResource {
671
1611
  }
672
1612
 
673
1613
  async #handleApplyDisableBalanceInitializationOperation(op, view, base, node, batch) {
674
- if (!this.check.validateCoreAdminOperation(op)) {
1614
+ if (!this.#stateValidationSchema.validateCoreAdminOperation(op)) {
675
1615
  this.#safeLogApply(OperationType.DISABLE_INITIALIZATION, "Schema validation failed.", node.from.key)
676
1616
  return Status.FAILURE;
677
1617
  };
@@ -744,8 +1684,8 @@ class State extends ReadyResource {
744
1684
  };
745
1685
 
746
1686
  // Verify signature
747
- const isMessageVerifed = tracCryptoApi.signature.verify(op.cao.is, hash, adminPublicKey);
748
- if (!isMessageVerifed) {
1687
+ const isMessageVerified = tracCryptoApi.signature.verify(op.cao.is, hash, adminPublicKey);
1688
+ if (!isMessageVerified) {
749
1689
  this.#safeLogApply(OperationType.DISABLE_INITIALIZATION, "Failed to verify message signature.", node.from.key)
750
1690
  return Status.FAILURE;
751
1691
  };
@@ -769,7 +1709,7 @@ class State extends ReadyResource {
769
1709
  return Status.FAILURE;
770
1710
  };
771
1711
 
772
- await batch.put(EntryType.INITIALIZATION, safeWriteUInt32BE(0, 0));
1712
+ await batch.put(EntryType.INITIALIZATION, safeWriteUInt32BE(0));
773
1713
  await batch.put(txHashHexString, node.value);
774
1714
 
775
1715
  return Status.SUCCESS;
@@ -782,7 +1722,7 @@ class State extends ReadyResource {
782
1722
  INITIALIZATION, THEIR STAKED BALANCE WILL BE REDUCED (PUNISHMENT).
783
1723
  */
784
1724
 
785
- if (!this.check.validateCoreAdminOperation(op)) {
1725
+ if (!this.#stateValidationSchema.validateCoreAdminOperation(op)) {
786
1726
  this.#safeLogApply(OperationType.ADD_ADMIN, "Contract schema validation failed.", node.from.key)
787
1727
  return Status.FAILURE;
788
1728
  };
@@ -829,9 +1769,9 @@ class State extends ReadyResource {
829
1769
  };
830
1770
 
831
1771
  // verify signature
832
- 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)
833
1773
  const txHashHexString = op.cao.tx.toString('hex');
834
- if (!isMessageVerifed) {
1774
+ if (!isMessageVerified) {
835
1775
  this.#safeLogApply(OperationType.ADD_ADMIN, "Failed to verify message signature.", node.from.key)
836
1776
  return Status.FAILURE;
837
1777
  };
@@ -908,7 +1848,7 @@ class State extends ReadyResource {
908
1848
 
909
1849
  // initialize admin entry and initialization flag
910
1850
  await batch.put(EntryType.ADMIN, newAdminEntry);
911
- await batch.put(EntryType.INITIALIZATION, safeWriteUInt32BE(1, 0));
1851
+ await batch.put(EntryType.INITIALIZATION, safeWriteUInt32BE(1));
912
1852
  await batch.put(txHashHexString, node.value);
913
1853
 
914
1854
  if (this.#config.enableTxApplyLogs) {
@@ -919,7 +1859,7 @@ class State extends ReadyResource {
919
1859
  }
920
1860
 
921
1861
  async #handleApplyAdminRecoveryOperation(op, view, base, node, batch) {
922
- if (!this.check.validateRoleAccessOperation(op)) {
1862
+ if (!this.#stateValidationSchema.validateRoleAccessOperation(op)) {
923
1863
  this.#safeLogApply(OperationType.ADMIN_RECOVERY, "Contract schema validation failed.", node.from.key)
924
1864
  return Status.FAILURE;
925
1865
  };
@@ -1170,7 +2110,7 @@ class State extends ReadyResource {
1170
2110
  }
1171
2111
 
1172
2112
  async #handleApplyAppendWhitelistOperation(op, view, base, node, batch) {
1173
- if (!this.check.validateAdminControlOperation(op)) {
2113
+ if (!this.#stateValidationSchema.validateAdminControlOperation(op)) {
1174
2114
  this.#safeLogApply(OperationType.APPEND_WHITELIST, "Contract schema validation failed.", node.from.key)
1175
2115
  return Status.FAILURE;
1176
2116
  };
@@ -1419,7 +2359,7 @@ class State extends ReadyResource {
1419
2359
  }
1420
2360
 
1421
2361
  async #handleApplyAddWriterOperation(op, view, base, node, batch) {
1422
- if (!this.check.validateRoleAccessOperation(op)) {
2362
+ if (!this.#stateValidationSchema.validateRoleAccessOperation(op)) {
1423
2363
  this.#safeLogApply(OperationType.ADD_WRITER, "Contract schema validation failed.", node.from.key)
1424
2364
  return Status.FAILURE;
1425
2365
  };
@@ -1723,7 +2663,7 @@ class State extends ReadyResource {
1723
2663
  }
1724
2664
 
1725
2665
  async #handleApplyRemoveWriterOperation(op, view, base, node, batch) {
1726
- if (!this.check.validateRoleAccessOperation(op)) {
2666
+ if (!this.#stateValidationSchema.validateRoleAccessOperation(op)) {
1727
2667
  this.#safeLogApply(OperationType.REMOVE_WRITER, "Contract schema validation failed.", node.from.key)
1728
2668
  return Status.FAILURE;
1729
2669
  };
@@ -1987,7 +2927,7 @@ class State extends ReadyResource {
1987
2927
  }
1988
2928
 
1989
2929
  async #handleApplyAddIndexerOperation(op, view, base, node, batch) {
1990
- if (!this.check.validateAdminControlOperation(op)) {
2930
+ if (!this.#stateValidationSchema.validateAdminControlOperation(op)) {
1991
2931
  this.#safeLogApply(OperationType.ADD_INDEXER, "Contract schema validation failed.", node.from.key)
1992
2932
  return Status.FAILURE;
1993
2933
  };
@@ -2073,9 +3013,9 @@ class State extends ReadyResource {
2073
3013
  return Status.FAILURE;
2074
3014
  };
2075
3015
 
2076
- const isMessageVerifed = tracCryptoApi.signature.verify(op.aco.is, hash, adminPublicKey);
3016
+ const isMessageVerified = tracCryptoApi.signature.verify(op.aco.is, hash, adminPublicKey);
2077
3017
  const txHashHexString = hash.toString('hex');
2078
- if (!isMessageVerifed) {
3018
+ if (!isMessageVerified) {
2079
3019
  this.#safeLogApply(OperationType.ADD_INDEXER, "Failed to verify message signature.", node.from.key)
2080
3020
  return Status.FAILURE;
2081
3021
  };
@@ -2205,7 +3145,7 @@ class State extends ReadyResource {
2205
3145
  }
2206
3146
 
2207
3147
  async #handleApplyRemoveIndexerOperation(op, view, base, node, batch) {
2208
- if (!this.check.validateAdminControlOperation(op)) {
3148
+ if (!this.#stateValidationSchema.validateAdminControlOperation(op)) {
2209
3149
  this.#safeLogApply(OperationType.REMOVE_INDEXER, "Contract schema validation failed.", node.from.key)
2210
3150
  return Status.FAILURE;
2211
3151
  };
@@ -2288,9 +3228,9 @@ class State extends ReadyResource {
2288
3228
  return Status.FAILURE;
2289
3229
  };
2290
3230
 
2291
- const isMessageVerifed = tracCryptoApi.signature.verify(op.aco.is, hash, adminPublicKey);
3231
+ const isMessageVerified = tracCryptoApi.signature.verify(op.aco.is, hash, adminPublicKey);
2292
3232
  const txHashHexString = hash.toString('hex');
2293
- if (!isMessageVerifed) {
3233
+ if (!isMessageVerified) {
2294
3234
  this.#safeLogApply(OperationType.REMOVE_INDEXER, "Failed to verify message signature.", node.from.key)
2295
3235
  return Status.FAILURE;
2296
3236
  };
@@ -2419,10 +3359,13 @@ class State extends ReadyResource {
2419
3359
  if (this.#config.enableTxApplyLogs) {
2420
3360
  console.info(`Indexer has been removed addr:wk:tx - ${toRemoveAddressString}:${decodedNodeEntry.wk.toString('hex')}:${txHashHexString}`);
2421
3361
  }
3362
+
3363
+ this.#emitEvent(CustomEventType.IS_NON_INDEXER, tracCryptoApi.address.decodeSafe(toRemoveAddressString))
3364
+ return Status.SUCCESS;
2422
3365
  }
2423
3366
 
2424
3367
  async #handleApplyBanValidatorOperation(op, view, base, node, batch) {
2425
- if (!this.check.validateAdminControlOperation(op)) {
3368
+ if (!this.#stateValidationSchema.validateAdminControlOperation(op)) {
2426
3369
  this.#safeLogApply(OperationType.BAN_VALIDATOR, "Contract schema validation failed.", node.from.key)
2427
3370
  return Status.FAILURE;
2428
3371
  };
@@ -2491,9 +3434,9 @@ class State extends ReadyResource {
2491
3434
  return Status.FAILURE;
2492
3435
  };
2493
3436
 
2494
- const isMessageVerifed = tracCryptoApi.signature.verify(op.aco.is, regeneratedHash, adminPublicKey);
3437
+ const isMessageVerified = tracCryptoApi.signature.verify(op.aco.is, regeneratedHash, adminPublicKey);
2495
3438
  const txHashHexString = regeneratedHash.toString('hex');
2496
- if (!isMessageVerifed) {
3439
+ if (!isMessageVerified) {
2497
3440
  this.#safeLogApply(OperationType.BAN_VALIDATOR, "Failed to verify message signature.", node.from.key)
2498
3441
  return Status.FAILURE;
2499
3442
  }
@@ -2623,7 +3566,7 @@ class State extends ReadyResource {
2623
3566
  }
2624
3567
 
2625
3568
  async #handleApplyBootstrapDeploymentOperation(op, view, base, node, batch) {
2626
- if (!this.check.validateBootstrapDeploymentOperation(op)) {
3569
+ if (!this.#stateValidationSchema.validateBootstrapDeploymentOperation(op)) {
2627
3570
  this.#safeLogApply(OperationType.BOOTSTRAP_DEPLOYMENT, "Contract schema validation failed.", node.from.key)
2628
3571
  return Status.FAILURE;
2629
3572
  };
@@ -2857,7 +3800,7 @@ class State extends ReadyResource {
2857
3800
 
2858
3801
  async #handleApplyTxOperation(op, view, base, node, batch) {
2859
3802
  // ATTENTION: The sanitization should be done before ANY other check, otherwise we risk crashing
2860
- if (!this.check.validateTransactionOperation(op)) {
3803
+ if (!this.#stateValidationSchema.validateTransactionOperation(op)) {
2861
3804
  this.#safeLogApply(OperationType.TX, "Contract schema validation failed.", node.from.key)
2862
3805
  return Status.FAILURE;
2863
3806
  };
@@ -3071,7 +4014,7 @@ class State extends ReadyResource {
3071
4014
  }
3072
4015
 
3073
4016
  async #handleApplyTransferOperation(op, view, base, node, batch) {
3074
- if (!this.check.validateTransferOperation(op)) {
4017
+ if (!this.#stateValidationSchema.validateTransferOperation(op)) {
3075
4018
  this.#safeLogApply(OperationType.TRANSFER, "Contract schema validation failed.", node.from.key)
3076
4019
  return Status.FAILURE;
3077
4020
  };
@@ -3108,7 +4051,7 @@ class State extends ReadyResource {
3108
4051
  if (b4a.equals(requesterPublicKey, NULL_BUFFER)) {
3109
4052
  this.#safeLogApply(OperationType.TRANSFER, "Error while decoding requester public key.", node.from.key)
3110
4053
  return Status.FAILURE;
3111
- };
4054
+ }
3112
4055
 
3113
4056
  // recreate requester message
3114
4057
  const requesterMessage = createMessage(
@@ -3262,18 +4205,302 @@ class State extends ReadyResource {
3262
4205
  await batch.put(requesterAddressString, transferResult.senderEntry);
3263
4206
  await batch.put(validatorAddressString, transferResult.validatorEntry);
3264
4207
 
3265
- if (!isSelfTransfer && !isRecipientValidator && transferResult.recipientEntry !== null) {
3266
- 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;
3267
4452
  }
3268
4453
 
3269
- 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);
3270
4457
 
3271
4458
  if (this.#config.enableTxApplyLogs) {
3272
- 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
3273
4460
  }
4461
+
4462
+ this.#emitEvent(CustomEventType.EPOCH_CREATED, { epoch: b4a.from(nextEpochBuffer), proposerAddress }); // notify epoch committed
3274
4463
  return Status.SUCCESS;
3275
4464
  }
3276
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
+
3277
4504
  async #transfer(senderAddressString, recipientAddressString, validatorAddressString, validatorEntryBuffer, transferAmountBuffer, feeAmountBuffer, isSelfTransfer, isRecipientValidator, batch, node) {
3278
4505
  if (!senderAddressString ||
3279
4506
  !recipientAddressString ||
@@ -3461,6 +4688,7 @@ class State extends ReadyResource {
3461
4688
  return null
3462
4689
  }
3463
4690
  }
4691
+
3464
4692
  async #isValidatorValidApply(validatorEntryBuffer, node, op) {
3465
4693
  // TODO: Maybe we should transfer all validator checks to this function (address, pubKey, signature, etc)
3466
4694
  if (validatorEntryBuffer === null) {
@@ -3507,7 +4735,7 @@ class State extends ReadyResource {
3507
4735
  if (initialization === null) {
3508
4736
  return false
3509
4737
  } else {
3510
- return b4a.equals(initialization, safeWriteUInt32BE(0, 0))
4738
+ return b4a.equals(initialization, safeWriteUInt32BE(0))
3511
4739
  }
3512
4740
  }
3513
4741
 
@@ -3786,12 +5014,6 @@ class State extends ReadyResource {
3786
5014
  return { newLicenseLength, decodedNewLicenseLength };
3787
5015
  }
3788
5016
 
3789
- #emitEvent(event, ...args) {
3790
- try {
3791
- this.emit(event, ...args)
3792
- } catch (_ignored) { }
3793
- }
3794
-
3795
5017
  async #transferFeeTxOperation(requesterAddressString, validatorAddressString, validatorEntryBuffer, subnetworkCreatorAddressString, feeAmount, batch, node) {
3796
5018
  if (!requesterAddressString ||
3797
5019
  !validatorAddressString ||
@@ -3945,6 +5167,400 @@ class State extends ReadyResource {
3945
5167
  };
3946
5168
  }
3947
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
+
3948
5564
  }
3949
5565
 
3950
5566
  export default State;