trac-msb 0.2.26 → 0.3.0-beta.2

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 +121 -0
  10. package/rpc/routes/v1.js +12 -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 +1803 -159
  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 +280 -67
  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
+ });
117
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
+ }
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
 
@@ -410,123 +666,813 @@ class State extends ReadyResource {
410
666
  if (startSeq > endSeq) {
411
667
  throw new Error("Invalid range");
412
668
  }
413
-
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;
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
+ } else if (entry.type === 'put' && entry.key.startsWith(EntryType.EPOCH_HASH)) {
680
+ // Epoch records share this history, but have their own key namespace.
681
+ const hash = entry.key.slice(EntryType.EPOCH_HASH.length);
682
+ if (hash.length === HASH_BYTE_LENGTH * 2 && isHexString(hash)) {
683
+ hashes.push({ hash, confirmed_length: entry.seq });
684
+ }
685
+ }
686
+ }
687
+
688
+ return hashes;
689
+ }
690
+
691
+ async getRegisteredWriterKey(writingKey) {
692
+ const entry = await this.get(EntryType.WRITER_ADDRESS + writingKey);
693
+ return entry ? addressUtils.addressToBuffer(entry, this.#config.addressPrefix) : null;
694
+ }
695
+
696
+ #setupHyperbee(store) {
697
+ this.#bee = new Hyperbee(store.get(TRAC_NAMESPACE), {
698
+ extension: false,
699
+ keyEncoding: HYPERBEE_KEY_ENCODING,
700
+ valueEncoding: HYPERBEE_VALUE_ENCODING,
701
+ })
702
+ return this.#bee;
703
+ }
704
+
705
+ async getSignedConsensusConfig() {
706
+ const currentConfigPointer = await this.getSigned(EntryType.CONSENSUS_CONFIG_CURRENT);
707
+ if (_.isNil(currentConfigPointer)) return null;
708
+
709
+ if (!b4a.isBuffer(currentConfigPointer) || currentConfigPointer.length !== 4) {
710
+ throw new Error('Invalid current consensus config pointer.');
711
+ }
712
+
713
+ const currentConfigIndex = currentConfigPointer.readUInt32BE(0);
714
+
715
+ const encodedConsensusConfig = await this.getSigned(EntryType.CONSENSUS_CONFIG_RECORD + currentConfigIndex);
716
+ if (_.isNil(encodedConsensusConfig)) {
717
+ throw new Error(`Consensus config record ${currentConfigIndex} does not exist.`);
718
+ }
719
+
720
+ return decodeVersionedConsensusConfig(encodedConsensusConfig);
721
+ }
722
+
723
+ async requireSignedConsensusConfig() {
724
+ const consensusConfig = await this.getSignedConsensusConfig();
725
+ if (_.isNil(consensusConfig)) {
726
+ throw new Error('Consensus config is not initialized.');
727
+ }
728
+ return consensusConfig;
729
+ }
730
+
731
+ /** @returns {Promise<import('../consensus/epochAppenderSelection/epochAppenderSelectionState.js').EpochAppenderSelectionContext>} Signed arbitration context. */
732
+ getEpochAppenderSelectionContext() {
733
+ return readEpochAppenderSelectionContext(this.#base, this.#config);
734
+ }
735
+
736
+ /**
737
+ * @param {import('../consensus/epochAppenderSelection/epochAppenderSelectionState.js').EpochAppendPublication} publication
738
+ * @returns {Promise<import('../consensus/epochAppenderSelection/epochAppenderSelectionState.js').EpochAppendOutcomeResult>}
739
+ */
740
+ getEpochAppendOutcome(publication) {
741
+ return readEpochAppendOutcome(this.#base, publication);
742
+ }
743
+
744
+ /** Authorizes the epoch append inside the writer queue before writing to Autobase. */
745
+ appendEpoch(payload, authorizeEpochAppend) {
746
+ return this.#writeQueue.add(async () => {
747
+ await authorizeEpochAppend({
748
+ writerKey: this.#base.local.key.toString('hex'),
749
+ from: this.#base.local.length,
750
+ });
751
+ return this.#base.append(payload);
752
+ });
753
+ }
754
+
755
+ // ATTENTION: DO NOT USE METHODS ABOVE IN APPLY PART!
756
+ ///////////////////////////////APPLY////////////////////////////////////
757
+
758
+ async #apply(nodes, view, base) {
759
+ const batch = view.batch();
760
+ const batchInvoker = nodes[0].from.key;
761
+ const postApplyEvents = [];
762
+
763
+
764
+ if (nodes.length > BATCH_SIZE) {
765
+ await this.#validatorPenaltyApply(batchInvoker, batch, base, OVERSIZED_BATCH_PENALTY_MULTIPLIER);
766
+ await batch.flush();
767
+ await batch.close();
768
+ return;
769
+ }
770
+
771
+ let invalidOperations = 0;
772
+
773
+ for (const node of nodes) {
774
+
775
+ if (b4a.byteLength(node.value) > transactionUtils.MAXIMUM_OPERATION_PAYLOAD_SIZE) {
776
+ this.#safeLogApply("Node payload exceeds the maximum operation payload size.", node.from.key)
777
+ invalidOperations++;
778
+ continue;
779
+ }
780
+
781
+ const op = safeDecodeApplyOperation(node.value);
782
+
783
+ if (!op) {
784
+ this.#safeLogApply("Failed to decode operation.", node.from.key)
785
+ invalidOperations++;
786
+ continue;
787
+ }
788
+
789
+ const handler = this.#getApplyOperationHandler(op.type);
790
+
791
+ if (handler) {
792
+ const result = await handler(op, view, base, node, batch, postApplyEvents);
793
+ if (result === Status.FAILURE) {
794
+ invalidOperations++;
795
+ } else if (result === Status.IGNORE) {
796
+ continue;
797
+ } else if (result !== Status.SUCCESS) {
798
+ this.#safeLogApply(`Unknown operation status: ${result}`, node.from.key);
799
+ invalidOperations++;
800
+ }
801
+ } else {
802
+ this.#safeLogApply(`Unknown operation type: ${op.type}`, node.from.key)
803
+ invalidOperations++;
804
+ }
805
+ }
806
+ if (invalidOperations > 0) {
807
+ await this.#validatorPenaltyApply(batchInvoker, batch, base, invalidOperations);
808
+ this.#safeLogApply(`Applied with ${invalidOperations} invalid operations.`)
809
+ }
810
+
811
+ await batch.flush();
812
+ await batch.close();
813
+
814
+ if (postApplyEvents.length !== 0) {
815
+ try {
816
+ this.#stateEventQueue.enqueue(postApplyEvents, view.core.length);
817
+ } catch (error) {
818
+ this.#safeLogApply('StateEventQueue', `Failed to queue post apply events: ${String(error)}`);
819
+ }
820
+ }
821
+ }
822
+
823
+ #getApplyOperationHandler(type) {
824
+ const handlers = {
825
+ [OperationType.BALANCE_INITIALIZATION]: this.#handleApplyInitializeBalanceOperation.bind(this),
826
+ [OperationType.DISABLE_INITIALIZATION]: this.#handleApplyDisableBalanceInitializationOperation.bind(this),
827
+ [OperationType.ADD_ADMIN]: this.#handleApplyAddAdminOperation.bind(this),
828
+ [OperationType.APPEND_WHITELIST]: this.#handleApplyAppendWhitelistOperation.bind(this),
829
+ [OperationType.ADD_WRITER]: this.#handleApplyAddWriterOperation.bind(this),
830
+ [OperationType.REMOVE_WRITER]: this.#handleApplyRemoveWriterOperation.bind(this),
831
+ [OperationType.ADMIN_RECOVERY]: this.#handleApplyAdminRecoveryOperation.bind(this),
832
+ [OperationType.ADD_INDEXER]: this.#handleApplyAddIndexerOperation.bind(this),
833
+ [OperationType.REMOVE_INDEXER]: this.#handleApplyRemoveIndexerOperation.bind(this),
834
+ [OperationType.BAN_VALIDATOR]: this.#handleApplyBanValidatorOperation.bind(this),
835
+ [OperationType.BOOTSTRAP_DEPLOYMENT]: this.#handleApplyBootstrapDeploymentOperation.bind(this),
836
+ [OperationType.TX]: this.#handleApplyTxOperation.bind(this),
837
+ [OperationType.TRANSFER]: this.#handleApplyTransferOperation.bind(this),
838
+ [OperationType.SET_EPOCH]: this.#handleApplySetEpochOperation.bind(this),
839
+ [OperationType.SET_GENESIS_EPOCH]: this.#handleApplySetGenesisEpoch.bind(this),
840
+ [OperationType.SET_CONSENSUS_CONFIG]: this.#handleApplySetConsensusConfig.bind(this),
841
+ [OperationType.HTLC_LOCK]: this.#handleApplyHtlcLockOperation.bind(this),
842
+ [OperationType.HTLC_CLAIM]: this.#handleApplyHtlcClaimOperation.bind(this),
843
+ [OperationType.HTLC_REFUND]: this.#handleApplyHtlcRefundOperation.bind(this),
844
+ };
845
+ return handlers[type] || null;
846
+ }
847
+
848
+ async #handleApplyHtlcLockOperation(op, view, base, node, batch) {
849
+ if (!this.#stateValidationSchema.validateHtlcLockOperation(op)) {
850
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Contract schema validation failed.", node.from.key);
851
+ return Status.FAILURE;
852
+ }
853
+
854
+ if (!Object.hasOwn(op.hlo, "vs") || !Object.hasOwn(op.hlo, "va") || !Object.hasOwn(op.hlo, "vn")) {
855
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Operation is not complete.", node.from.key);
856
+ return Status.FAILURE;
857
+ }
858
+
859
+ if (b4a.equals(op.address, op.hlo.va)) {
860
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Validator cannot sign its own transaction.", node.from.key);
861
+ return Status.FAILURE;
862
+ }
863
+
864
+ if (b4a.equals(op.hlo.in, op.hlo.vn)) {
865
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Nonces should not be the same.", node.from.key);
866
+ return Status.FAILURE;
867
+ }
868
+
869
+ if (b4a.equals(op.hlo.is, op.hlo.vs)) {
870
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Signatures should not be the same.", node.from.key);
871
+ return Status.FAILURE;
872
+ }
873
+
874
+ const lockerAddressString = addressUtils.bufferToAddress(op.address, this.#config.addressPrefix);
875
+ if (lockerAddressString === null) {
876
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Locker address is invalid.", node.from.key);
877
+ return Status.FAILURE;
878
+ }
879
+
880
+ const lockerPublicKey = tracCryptoApi.address.decodeSafe(lockerAddressString);
881
+ if (
882
+ !b4a.isBuffer(lockerPublicKey) ||
883
+ lockerPublicKey.length !== PUBLIC_KEY_LENGTH ||
884
+ isZeroBuffer(lockerPublicKey)
885
+ ) {
886
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Failed to decode locker public key.", node.from.key);
887
+ return Status.FAILURE;
888
+ }
889
+
890
+ let lockerMessage;
891
+ try {
892
+ lockerMessage = createHtlcLockSigningMessage(
893
+ this.#config.networkId,
894
+ op.address,
895
+ op.hlo
896
+ );
897
+ } catch {
898
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Invalid locker message.", node.from.key);
899
+ return Status.FAILURE;
900
+ }
901
+
902
+ const regeneratedTxHash = await tracCryptoApi.hash.blake3Safe(lockerMessage);
903
+ if (!isBufferValid(regeneratedTxHash, HASH_BYTE_LENGTH)) {
904
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Failed to hash locker message.", node.from.key);
905
+ return Status.FAILURE;
906
+ }
907
+ if (!b4a.equals(regeneratedTxHash, op.hlo.tx)) {
908
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Message hash does not match the tx_hash.", node.from.key);
909
+ return Status.FAILURE;
910
+ }
911
+
912
+ if (!tracCryptoApi.signature.verify(op.hlo.is, regeneratedTxHash, lockerPublicKey)) {
913
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Failed to verify locker signature.", node.from.key);
914
+ return Status.FAILURE;
915
+ }
916
+
917
+ if (!b4a.equals(lockerPublicKey, op.hlo.ss[0])) {
918
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Signer zero does not match the locker.", node.from.key);
919
+ return Status.FAILURE;
920
+ }
921
+
922
+ if (
923
+ 1 + op.hlo.cs.length < op.hlo.th[0] ||
924
+ !verifyOrderedHtlcCosignerSignatures(op.hlo.cs, op.hlo.tx, op.hlo.ss)
925
+ ) {
926
+ this.#safeLogApply(OperationType.HTLC_LOCK, "HTLC cosigner authorization is invalid.", node.from.key);
927
+ return Status.FAILURE;
928
+ }
929
+
930
+ const validatorAddressString = addressUtils.bufferToAddress(op.hlo.va, this.#config.addressPrefix);
931
+ if (validatorAddressString === null) {
932
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Validator address is invalid.", node.from.key);
933
+ return Status.FAILURE;
934
+ }
935
+
936
+ const validatorPublicKey = tracCryptoApi.address.decodeSafe(validatorAddressString);
937
+ if (
938
+ !b4a.isBuffer(validatorPublicKey) ||
939
+ validatorPublicKey.length !== PUBLIC_KEY_LENGTH ||
940
+ isZeroBuffer(validatorPublicKey)
941
+ ) {
942
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Failed to decode validator public key.", node.from.key);
943
+ return Status.FAILURE;
944
+ }
945
+
946
+ const validatorMessage = createMessage(
947
+ this.#config.networkId,
948
+ op.hlo.tx,
949
+ op.hlo.vn,
950
+ OperationType.HTLC_LOCK
951
+ );
952
+ if (validatorMessage.length === 0) {
953
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Invalid validator message.", node.from.key);
954
+ return Status.FAILURE;
955
+ }
956
+
957
+ const validatorMessageHash = await tracCryptoApi.hash.blake3Safe(validatorMessage);
958
+ if (!isBufferValid(validatorMessageHash, HASH_BYTE_LENGTH)) {
959
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Failed to hash validator message.", node.from.key);
960
+ return Status.FAILURE;
961
+ }
962
+ if (!tracCryptoApi.signature.verify(op.hlo.vs, validatorMessageHash, validatorPublicKey)) {
963
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Failed to verify validator signature.", node.from.key);
964
+ return Status.FAILURE;
965
+ }
966
+
967
+ const indexersSequenceState = await this.#getIndexerSequenceStateApply(base);
968
+ if (indexersSequenceState === null) {
969
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Indexer sequence state is invalid.", node.from.key);
970
+ return Status.FAILURE;
971
+ }
972
+ if (!b4a.equals(op.hlo.txv, indexersSequenceState)) {
973
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Transaction was not executed.", node.from.key);
974
+ return Status.FAILURE;
975
+ }
976
+
977
+ const validatorEntryBuffer = await this.#getEntryApply(validatorAddressString, batch);
978
+ if (!await this.#isValidatorValidApply(validatorEntryBuffer, node, op)) {
979
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Validator consistency check failed.", node.from.key);
980
+ return Status.FAILURE;
981
+ }
982
+
983
+ const lockIdHex = b4a.toString(op.hlo.tx, 'hex');
984
+ const operationEntry = await this.#getEntryApply(lockIdHex, batch);
985
+ if (operationEntry !== null) {
986
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Operation has already been applied.", node.from.key);
987
+ return Status.IGNORE;
988
+ }
989
+
990
+ const escrowKey = EntryType.HTLC_ESCROW + lockIdHex;
991
+ const existingEscrow = await this.#getEntryApply(escrowKey, batch);
992
+ if (existingEscrow !== null) {
993
+ this.#safeLogApply(OperationType.HTLC_LOCK, "HTLC escrow already exists.", node.from.key);
994
+ return Status.FAILURE;
995
+ }
996
+
997
+ const currentEpochBuffer = await this.#getEntryApply(EntryType.EPOCH_CURRENT, batch);
998
+ if (!isBufferValid(currentEpochBuffer, EPOCH_BYTE_LENGTH)) {
999
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Current epoch is not initialized or is invalid.", node.from.key);
1000
+ return Status.FAILURE;
1001
+ }
1002
+
1003
+ const currentEpoch = currentEpochBuffer.readBigUInt64BE(0);
1004
+ const refundEpoch = op.hlo.re.readBigUInt64BE(0);
1005
+ if (refundEpoch < currentEpoch + HTLC_MIN_LOCK_DURATION_EPOCHS) {
1006
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Refund epoch is below the minimum lock duration.", node.from.key);
1007
+ return Status.IGNORE;
1008
+ }
1009
+
1010
+ const principal = toBalance(op.hlo.am);
1011
+ const surchargeFee = toBalance(op.hlo.fa);
1012
+ const networkFee = toBalance(transactionUtils.FEE);
1013
+ if (principal === null || surchargeFee === null || networkFee === null) {
1014
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Invalid HTLC or network fee amount.", node.from.key);
1015
+ return Status.FAILURE;
1016
+ }
1017
+
1018
+ const escrowAmount = principal.add(surchargeFee);
1019
+ const totalDeductedAmount = escrowAmount?.add(networkFee) ?? null;
1020
+ if (escrowAmount === null || totalDeductedAmount === null) {
1021
+ this.#safeLogApply(OperationType.HTLC_LOCK, "HTLC total amount overflow.", node.from.key);
1022
+ return Status.FAILURE;
1023
+ }
1024
+
1025
+ const lockerEntryBuffer = await this.#getEntryApply(lockerAddressString, batch);
1026
+ const lockerEntry = nodeEntryUtils.decode(lockerEntryBuffer);
1027
+ if (lockerEntry === null) {
1028
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Locker account does not exist or is invalid.", node.from.key);
1029
+ return Status.FAILURE;
1030
+ }
1031
+
1032
+ const lockerBalance = toBalance(lockerEntry.balance);
1033
+ if (lockerBalance === null) {
1034
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Locker balance is invalid.", node.from.key);
1035
+ return Status.FAILURE;
1036
+ }
1037
+ if (!lockerBalance.greaterThanOrEquals(totalDeductedAmount)) {
1038
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Insufficient locker balance.", node.from.key);
1039
+ return Status.IGNORE;
1040
+ }
1041
+
1042
+ const updatedLockerBalance = lockerBalance.sub(totalDeductedAmount);
1043
+ const updatedLockerEntry = updatedLockerBalance?.update(lockerEntryBuffer) ?? null;
1044
+ if (updatedLockerEntry === null) {
1045
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Failed to update locker balance.", node.from.key);
1046
+ return Status.FAILURE;
1047
+ }
1048
+
1049
+ const validatorEntry = nodeEntryUtils.decode(validatorEntryBuffer);
1050
+ const validatorBalance = validatorEntry ? toBalance(validatorEntry.balance) : null;
1051
+ const validatorReward = networkFee.percentage(PERCENT_75);
1052
+ if (validatorBalance === null || validatorReward === null) {
1053
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Validator balance or reward is invalid.", node.from.key);
1054
+ return Status.FAILURE;
1055
+ }
1056
+
1057
+ const updatedValidatorBalance = validatorBalance.add(validatorReward);
1058
+ const updatedValidatorEntry = updatedValidatorBalance?.update(validatorEntryBuffer) ?? null;
1059
+ if (updatedValidatorEntry === null) {
1060
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Failed to update validator balance.", node.from.key);
1061
+ return Status.FAILURE;
1062
+ }
1063
+
1064
+ const escrowEntry = escrowEntryUtils.init({
1065
+ lockId: op.hlo.tx,
1066
+ lockerAddress: op.address,
1067
+ claimRecipientAddress: op.hlo.ca,
1068
+ refundRecipientAddress: op.hlo.ra,
1069
+ amount: op.hlo.am,
1070
+ additionalFeeAmount: op.hlo.fa,
1071
+ additionalFeeRecipientAddress: op.hlo.fr,
1072
+ hashLock: op.hlo.hl,
1073
+ refundEpoch: op.hlo.re,
1074
+ metadata: op.hlo.md,
1075
+ }, this.#config.addressPrefix);
1076
+ if (escrowEntry.length === 0) {
1077
+ this.#safeLogApply(OperationType.HTLC_LOCK, "Failed to encode HTLC escrow.", node.from.key);
1078
+ return Status.FAILURE;
1079
+ }
1080
+
1081
+ await batch.put(lockerAddressString, updatedLockerEntry);
1082
+ await batch.put(validatorAddressString, updatedValidatorEntry);
1083
+ await batch.put(escrowKey, escrowEntry);
1084
+ await batch.put(lockIdHex, node.value);
1085
+
1086
+ if (this.#config.enableTxApplyLogs) {
1087
+ console.info(`HTLC lock operation: ${lockIdHex} has been appended.`);
1088
+ }
1089
+ return Status.SUCCESS;
1090
+ }
1091
+
1092
+ async #handleApplyHtlcClaimOperation(op, view, base, node, batch) {
1093
+ if (!this.#stateValidationSchema.validateHtlcClaimOperation(op)) {
1094
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Contract schema validation failed.", node.from.key);
1095
+ return Status.FAILURE;
1096
+ }
1097
+ if (!Object.hasOwn(op.hco, "vs") || !Object.hasOwn(op.hco, "va") || !Object.hasOwn(op.hco, "vn")) {
1098
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Operation is not complete.", node.from.key);
1099
+ return Status.FAILURE;
1100
+ }
1101
+ if (b4a.equals(op.address, op.hco.va)) {
1102
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Validator cannot sign its own transaction.", node.from.key);
1103
+ return Status.FAILURE;
1104
+ }
1105
+ if (b4a.equals(op.hco.in, op.hco.vn)) {
1106
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Nonces should not be the same.", node.from.key);
1107
+ return Status.FAILURE;
1108
+ }
1109
+ if (b4a.equals(op.hco.is, op.hco.vs)) {
1110
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Signatures should not be the same.", node.from.key);
1111
+ return Status.FAILURE;
1112
+ }
1113
+
1114
+ const submitterAddress = addressUtils.bufferToAddress(op.address, this.#config.addressPrefix);
1115
+ if (submitterAddress === null) {
1116
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Submitter address is invalid.", node.from.key);
1117
+ return Status.FAILURE;
1118
+ }
1119
+
1120
+ const submitterPublicKey = tracCryptoApi.address.decodeSafe(submitterAddress);
1121
+ if (!isBufferValid(submitterPublicKey, PUBLIC_KEY_LENGTH) || isZeroBuffer(submitterPublicKey)) {
1122
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Submitter public key is invalid.", node.from.key);
1123
+ return Status.FAILURE;
1124
+ }
1125
+
1126
+ const message = createMessage(
1127
+ this.#config.networkId,
1128
+ op.hco.txv,
1129
+ op.hco.li,
1130
+ op.hco.pi,
1131
+ op.hco.in,
1132
+ OperationType.HTLC_CLAIM
1133
+ );
1134
+ if (message.length === 0) {
1135
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Invalid claim message.", node.from.key);
1136
+ return Status.FAILURE;
1137
+ }
1138
+
1139
+ const txHash = await tracCryptoApi.hash.blake3Safe(message);
1140
+ if (!isBufferValid(txHash, HASH_BYTE_LENGTH)) {
1141
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Failed to hash claim message.", node.from.key);
1142
+ return Status.FAILURE;
1143
+ }
1144
+ if (!b4a.equals(txHash, op.hco.tx)) {
1145
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Claim transaction hash is invalid.", node.from.key);
1146
+ return Status.FAILURE;
1147
+ }
1148
+ if (!tracCryptoApi.signature.verify(op.hco.is, txHash, submitterPublicKey)) {
1149
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Claim submitter signature is invalid.", node.from.key);
1150
+ return Status.FAILURE;
1151
+ }
1152
+
1153
+ const validatorAddress = addressUtils.bufferToAddress(op.hco.va, this.#config.addressPrefix);
1154
+ if (validatorAddress === null) {
1155
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Validator address is invalid.", node.from.key);
1156
+ return Status.FAILURE;
1157
+ }
1158
+ const validatorPublicKey = tracCryptoApi.address.decodeSafe(validatorAddress);
1159
+ if (!isBufferValid(validatorPublicKey, PUBLIC_KEY_LENGTH) || isZeroBuffer(validatorPublicKey)) {
1160
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Validator public key is invalid.", node.from.key);
1161
+ return Status.FAILURE;
1162
+ }
1163
+ const validatorMessage = createMessage(
1164
+ this.#config.networkId,
1165
+ op.hco.tx,
1166
+ op.hco.vn,
1167
+ OperationType.HTLC_CLAIM
1168
+ );
1169
+ if (validatorMessage.length === 0) {
1170
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Invalid validator message.", node.from.key);
1171
+ return Status.FAILURE;
1172
+ }
1173
+ const validatorHash = await tracCryptoApi.hash.blake3Safe(validatorMessage);
1174
+ if (!isBufferValid(validatorHash, HASH_BYTE_LENGTH)) {
1175
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Failed to hash validator message.", node.from.key);
1176
+ return Status.FAILURE;
1177
+ }
1178
+ if (!tracCryptoApi.signature.verify(op.hco.vs, validatorHash, validatorPublicKey)) {
1179
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Validator signature is invalid.", node.from.key);
1180
+ return Status.FAILURE;
1181
+ }
1182
+ const validatorEntry = await this.#getEntryApply(validatorAddress, batch);
1183
+ if (!await this.#isValidatorValidApply(validatorEntry, node, op)) {
1184
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Validator consistency check failed.", node.from.key);
1185
+ return Status.FAILURE;
1186
+ }
1187
+
1188
+ const indexersSequenceState = await this.#getIndexerSequenceStateApply(base);
1189
+ if (indexersSequenceState === null || !b4a.equals(op.hco.txv, indexersSequenceState)) {
1190
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Transaction was not executed.", node.from.key);
1191
+ return Status.FAILURE;
1192
+ }
1193
+
1194
+ const claimTxHashHex = b4a.toString(op.hco.tx, 'hex');
1195
+ if (await this.#getEntryApply(claimTxHashHex, batch) !== null) {
1196
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Operation has already been applied.", node.from.key);
1197
+ return Status.IGNORE;
1198
+ }
1199
+
1200
+ const escrowKey = EntryType.HTLC_ESCROW + b4a.toString(op.hco.li, 'hex');
1201
+ const escrowBuffer = await this.#getEntryApply(escrowKey, batch);
1202
+ if (escrowBuffer === null) {
1203
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "HTLC escrow does not exist.", node.from.key);
1204
+ return Status.FAILURE;
1205
+ }
1206
+ const escrow = escrowEntryUtils.decode(escrowBuffer, this.#config.addressPrefix);
1207
+ if (!escrow || !b4a.equals(escrow.lockId, op.hco.li)) {
1208
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "HTLC escrow is invalid.", node.from.key);
1209
+ return Status.FAILURE;
1210
+ }
1211
+ if (escrow.status !== escrowEntryUtils.Status.PENDING) {
1212
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "HTLC escrow is already settled.", node.from.key);
1213
+ return Status.IGNORE;
1214
+ }
1215
+
1216
+ const currentEpoch = await this.#getEntryApply(EntryType.EPOCH_CURRENT, batch);
1217
+ if (!isBufferValid(currentEpoch, EPOCH_BYTE_LENGTH)) {
1218
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Current epoch is not initialized or is invalid.", node.from.key);
1219
+ return Status.FAILURE;
1220
+ }
1221
+ if (b4a.compare(currentEpoch, escrow.refundEpoch) >= 0) {
1222
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Claim window has expired.", node.from.key);
1223
+ return Status.IGNORE;
1224
+ }
1225
+
1226
+ const settlement = escrowEntryUtils.makeClaim(escrowBuffer, op.hco.pi, currentEpoch, this.#config.addressPrefix);
1227
+ if (settlement === null) {
1228
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "HTLC preimage or escrow transition is invalid.", node.from.key);
1229
+ return Status.FAILURE;
1230
+ }
1231
+
1232
+ const payouts = [
1233
+ [settlement.address, settlement.amount],
1234
+ [settlement.additionalFeeRecipientAddress, settlement.additionalFeeAmount],
1235
+ ];
1236
+ const credits = new Map();
1237
+ for (const [addressBytes, amount] of payouts) {
1238
+ if (addressBytes === null) continue;
1239
+ const address = addressUtils.bufferToAddress(addressBytes, this.#config.addressPrefix);
1240
+ if (address === null || amount === null) {
1241
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Settlement recipient or amount is invalid.", node.from.key);
1242
+ return Status.FAILURE;
1243
+ }
1244
+ const total = credits.has(address) ? credits.get(address).add(amount) : amount;
1245
+ if (total === null) {
1246
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Settlement amount overflow.", node.from.key);
1247
+ return Status.FAILURE;
1248
+ }
1249
+ credits.set(address, total);
1250
+ }
1251
+
1252
+ const updatedRecipients = new Map();
1253
+ for (const [address, amount] of credits) {
1254
+ const entryBuffer = await this.#getEntryApply(address, batch);
1255
+ let updatedEntry;
1256
+ if (entryBuffer === null) {
1257
+ updatedEntry = nodeEntryUtils.init(ZERO_WK, nodeRoleUtils.NodeRole.READER, amount.value);
1258
+ } else {
1259
+ const entry = nodeEntryUtils.decode(entryBuffer);
1260
+ if (entry === null) {
1261
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Settlement recipient account is invalid.", node.from.key);
1262
+ return Status.FAILURE;
1263
+ }
1264
+ const balance = toBalance(entry.balance);
1265
+ if (balance === null) {
1266
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Settlement recipient balance is invalid.", node.from.key);
1267
+ return Status.FAILURE;
1268
+ }
1269
+ const updatedBalance = balance.add(amount);
1270
+ if (updatedBalance === null) {
1271
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Settlement recipient balance overflow.", node.from.key);
1272
+ return Status.FAILURE;
1273
+ }
1274
+ updatedEntry = updatedBalance.update(entryBuffer);
1275
+ }
1276
+ if (!isBufferValid(updatedEntry, NODE_ENTRY_SIZE)) {
1277
+ this.#safeLogApply(OperationType.HTLC_CLAIM, "Failed to credit settlement recipient.", node.from.key);
1278
+ return Status.FAILURE;
1279
+ }
1280
+ updatedRecipients.set(address, updatedEntry);
1281
+ }
1282
+
1283
+ for (const [address, entry] of updatedRecipients) await batch.put(address, entry);
1284
+ await batch.put(escrowKey, settlement.claimedEscrowEntry);
1285
+ await batch.put(claimTxHashHex, node.value);
1286
+
1287
+ if (this.#config.enableTxApplyLogs) {
1288
+ console.info(`HTLC claim operation: ${claimTxHashHex} has been appended.`);
1289
+ }
1290
+ return Status.SUCCESS;
1291
+ }
1292
+
1293
+ async #handleApplyHtlcRefundOperation(op, view, base, node, batch) {
1294
+ if (!this.#stateValidationSchema.validateHtlcRefundOperation(op)) {
1295
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Contract schema validation failed.", node.from.key);
1296
+ return Status.FAILURE;
1297
+ }
1298
+ if (!Object.hasOwn(op.hro, "vs") || !Object.hasOwn(op.hro, "va") || !Object.hasOwn(op.hro, "vn")) {
1299
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Operation is not complete.", node.from.key);
1300
+ return Status.FAILURE;
1301
+ }
1302
+ if (b4a.equals(op.address, op.hro.va)) {
1303
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Validator cannot sign its own transaction.", node.from.key);
1304
+ return Status.FAILURE;
1305
+ }
1306
+ if (b4a.equals(op.hro.in, op.hro.vn)) {
1307
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Nonces should not be the same.", node.from.key);
1308
+ return Status.FAILURE;
1309
+ }
1310
+ if (b4a.equals(op.hro.is, op.hro.vs)) {
1311
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Signatures should not be the same.", node.from.key);
1312
+ return Status.FAILURE;
1313
+ }
1314
+
1315
+ const submitterAddress = addressUtils.bufferToAddress(op.address, this.#config.addressPrefix);
1316
+ if (submitterAddress === null) {
1317
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Submitter address is invalid.", node.from.key);
1318
+ return Status.FAILURE;
1319
+ }
1320
+ const submitterPublicKey = tracCryptoApi.address.decodeSafe(submitterAddress);
1321
+ if (!isBufferValid(submitterPublicKey, PUBLIC_KEY_LENGTH) || isZeroBuffer(submitterPublicKey)) {
1322
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Submitter public key is invalid.", node.from.key);
1323
+ return Status.FAILURE;
1324
+ }
1325
+
1326
+ const message = createMessage(
1327
+ this.#config.networkId,
1328
+ op.hro.txv,
1329
+ op.hro.li,
1330
+ op.hro.in,
1331
+ OperationType.HTLC_REFUND
1332
+ );
1333
+ if (message.length === 0) {
1334
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Invalid refund message.", node.from.key);
1335
+ return Status.FAILURE;
1336
+ }
1337
+ const txHash = await tracCryptoApi.hash.blake3Safe(message);
1338
+ if (!isBufferValid(txHash, HASH_BYTE_LENGTH)) {
1339
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Failed to hash refund message.", node.from.key);
1340
+ return Status.FAILURE;
1341
+ }
1342
+ if (!b4a.equals(txHash, op.hro.tx)) {
1343
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Refund transaction hash is invalid.", node.from.key);
1344
+ return Status.FAILURE;
1345
+ }
1346
+ if (!tracCryptoApi.signature.verify(op.hro.is, txHash, submitterPublicKey)) {
1347
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Refund submitter signature is invalid.", node.from.key);
1348
+ return Status.FAILURE;
1349
+ }
1350
+
1351
+ const validatorAddress = addressUtils.bufferToAddress(op.hro.va, this.#config.addressPrefix);
1352
+ if (validatorAddress === null) {
1353
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Validator address is invalid.", node.from.key);
1354
+ return Status.FAILURE;
1355
+ }
1356
+ const validatorPublicKey = tracCryptoApi.address.decodeSafe(validatorAddress);
1357
+ if (!isBufferValid(validatorPublicKey, PUBLIC_KEY_LENGTH) || isZeroBuffer(validatorPublicKey)) {
1358
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Validator public key is invalid.", node.from.key);
1359
+ return Status.FAILURE;
1360
+ }
1361
+ const validatorMessage = createMessage(
1362
+ this.#config.networkId,
1363
+ op.hro.tx,
1364
+ op.hro.vn,
1365
+ OperationType.HTLC_REFUND
1366
+ );
1367
+ if (validatorMessage.length === 0) {
1368
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Invalid validator message.", node.from.key);
1369
+ return Status.FAILURE;
1370
+ }
1371
+ const validatorHash = await tracCryptoApi.hash.blake3Safe(validatorMessage);
1372
+ if (!isBufferValid(validatorHash, HASH_BYTE_LENGTH)) {
1373
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Failed to hash validator message.", node.from.key);
1374
+ return Status.FAILURE;
1375
+ }
1376
+ if (!tracCryptoApi.signature.verify(op.hro.vs, validatorHash, validatorPublicKey)) {
1377
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Validator signature is invalid.", node.from.key);
1378
+ return Status.FAILURE;
1379
+ }
1380
+ const validatorEntry = await this.#getEntryApply(validatorAddress, batch);
1381
+ if (!await this.#isValidatorValidApply(validatorEntry, node, op)) {
1382
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Validator consistency check failed.", node.from.key);
1383
+ return Status.FAILURE;
424
1384
  }
425
1385
 
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
- }
1386
+ const indexersSequenceState = await this.#getIndexerSequenceStateApply(base);
1387
+ if (indexersSequenceState === null || !b4a.equals(op.hro.txv, indexersSequenceState)) {
1388
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Transaction was not executed.", node.from.key);
1389
+ return Status.FAILURE;
431
1390
  }
432
1391
 
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
-
1392
+ const refundTxHashHex = b4a.toString(op.hro.tx, 'hex');
1393
+ if (await this.#getEntryApply(refundTxHashHex, batch) !== null) {
1394
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Operation has already been applied.", node.from.key);
1395
+ return Status.IGNORE;
1396
+ }
457
1397
 
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;
1398
+ const escrowKey = EntryType.HTLC_ESCROW + b4a.toString(op.hro.li, 'hex');
1399
+ const escrowBuffer = await this.#getEntryApply(escrowKey, batch);
1400
+ if (escrowBuffer === null) {
1401
+ this.#safeLogApply(OperationType.HTLC_REFUND, "HTLC escrow does not exist.", node.from.key);
1402
+ return Status.FAILURE;
1403
+ }
1404
+ const escrow = escrowEntryUtils.decode(escrowBuffer, this.#config.addressPrefix);
1405
+ if (!escrow || !b4a.equals(escrow.lockId, op.hro.li)) {
1406
+ this.#safeLogApply(OperationType.HTLC_REFUND, "HTLC escrow is invalid.", node.from.key);
1407
+ return Status.FAILURE;
1408
+ }
1409
+ if (escrow.status !== escrowEntryUtils.Status.PENDING) {
1410
+ this.#safeLogApply(OperationType.HTLC_REFUND, "HTLC escrow is already settled.", node.from.key);
1411
+ return Status.IGNORE;
463
1412
  }
464
1413
 
465
- let invalidOperations = 0;
1414
+ const currentEpoch = await this.#getEntryApply(EntryType.EPOCH_CURRENT, batch);
1415
+ if (!isBufferValid(currentEpoch, EPOCH_BYTE_LENGTH)) {
1416
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Current epoch is not initialized or is invalid.", node.from.key);
1417
+ return Status.FAILURE;
1418
+ }
466
1419
 
467
- for (const node of nodes) {
1420
+ if (b4a.compare(currentEpoch, escrow.refundEpoch) < 0) {
1421
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Refund epoch has not been reached.", node.from.key);
1422
+ return Status.IGNORE;
1423
+ }
468
1424
 
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
- };
1425
+ const settlement = escrowEntryUtils.makeRefund(escrowBuffer, currentEpoch, this.#config.addressPrefix);
1426
+ if (settlement === null) {
1427
+ this.#safeLogApply(OperationType.HTLC_REFUND, "HTLC refund transition is invalid.", node.from.key);
1428
+ return Status.FAILURE;
1429
+ }
474
1430
 
475
- const op = safeDecodeApplyOperation(node.value);
1431
+ const recipientAddress = addressUtils.bufferToAddress(settlement.address, this.#config.addressPrefix);
1432
+ if (recipientAddress === null || settlement.amount === null) {
1433
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Refund recipient or amount is invalid.", node.from.key);
1434
+ return Status.FAILURE;
1435
+ }
476
1436
 
477
- if (!op) {
478
- this.#safeLogApply("Failed to decode operation.", node.from.key)
479
- invalidOperations++;
480
- continue;
1437
+ const recipientEntryBuffer = await this.#getEntryApply(recipientAddress, batch);
1438
+ let updatedRecipientEntry;
1439
+ if (recipientEntryBuffer === null) {
1440
+ updatedRecipientEntry = nodeEntryUtils.init(ZERO_WK, nodeRoleUtils.NodeRole.READER, settlement.amount.value);
1441
+ } else {
1442
+ const recipientEntry = nodeEntryUtils.decode(recipientEntryBuffer);
1443
+ if (recipientEntry === null) {
1444
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Refund recipient account is invalid.", node.from.key);
1445
+ return Status.FAILURE;
481
1446
  }
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++;
1447
+ const recipientBalance = toBalance(recipientEntry.balance);
1448
+ if (recipientBalance === null) {
1449
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Refund recipient balance is invalid.", node.from.key);
1450
+ return Status.FAILURE;
498
1451
  }
1452
+ const updatedBalance = recipientBalance.add(settlement.amount);
1453
+ if (updatedBalance === null) {
1454
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Refund recipient balance overflow.", node.from.key);
1455
+ return Status.FAILURE;
1456
+ }
1457
+ updatedRecipientEntry = updatedBalance.update(recipientEntryBuffer);
499
1458
  }
500
- if (invalidOperations > 0) {
501
- await this.#validatorPenaltyApply(batchInvoker, batch, base, invalidOperations);
502
- this.#safeLogApply(`Applied with ${invalidOperations} invalid operations.`)
1459
+ if (!isBufferValid(updatedRecipientEntry, NODE_ENTRY_SIZE)) {
1460
+ this.#safeLogApply(OperationType.HTLC_REFUND, "Failed to credit refund recipient.", node.from.key);
1461
+ return Status.FAILURE;
503
1462
  }
504
1463
 
505
- await batch.flush();
506
- await batch.close();
507
- }
1464
+ await batch.put(recipientAddress, updatedRecipientEntry);
1465
+ await batch.put(escrowKey, settlement.refundedEscrowEntry);
1466
+ await batch.put(refundTxHashHex, node.value);
508
1467
 
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;
1468
+ if (this.#config.enableTxApplyLogs) {
1469
+ console.info(`HTLC refund operation: ${refundTxHashHex} has been appended.`);
1470
+ }
1471
+ return Status.SUCCESS;
526
1472
  }
527
1473
 
528
1474
  async #handleApplyInitializeBalanceOperation(op, view, base, node, batch) {
529
- if (!this.check.validateBalanceInitialization(op)) {
1475
+ if (!this.#stateValidationSchema.validateBalanceInitialization(op)) {
530
1476
  this.#safeLogApply(OperationType.BALANCE_INITIALIZATION, "Contract schema validation failed.", node.from.key)
531
1477
  return Status.FAILURE;
532
1478
  };
@@ -622,8 +1568,8 @@ class State extends ReadyResource {
622
1568
  };
623
1569
 
624
1570
  // Verify signature
625
- const isMessageVerifed = tracCryptoApi.signature.verify(op.bio.is, hash, adminPublicKey);
626
- if (!isMessageVerifed) {
1571
+ const isMessageVerified = tracCryptoApi.signature.verify(op.bio.is, hash, adminPublicKey);
1572
+ if (!isMessageVerified) {
627
1573
  this.#safeLogApply(OperationType.BALANCE_INITIALIZATION, "Failed to verify message signature.", node.from.key)
628
1574
  return Status.FAILURE;
629
1575
  };
@@ -671,7 +1617,7 @@ class State extends ReadyResource {
671
1617
  }
672
1618
 
673
1619
  async #handleApplyDisableBalanceInitializationOperation(op, view, base, node, batch) {
674
- if (!this.check.validateCoreAdminOperation(op)) {
1620
+ if (!this.#stateValidationSchema.validateCoreAdminOperation(op)) {
675
1621
  this.#safeLogApply(OperationType.DISABLE_INITIALIZATION, "Schema validation failed.", node.from.key)
676
1622
  return Status.FAILURE;
677
1623
  };
@@ -744,8 +1690,8 @@ class State extends ReadyResource {
744
1690
  };
745
1691
 
746
1692
  // Verify signature
747
- const isMessageVerifed = tracCryptoApi.signature.verify(op.cao.is, hash, adminPublicKey);
748
- if (!isMessageVerifed) {
1693
+ const isMessageVerified = tracCryptoApi.signature.verify(op.cao.is, hash, adminPublicKey);
1694
+ if (!isMessageVerified) {
749
1695
  this.#safeLogApply(OperationType.DISABLE_INITIALIZATION, "Failed to verify message signature.", node.from.key)
750
1696
  return Status.FAILURE;
751
1697
  };
@@ -769,7 +1715,7 @@ class State extends ReadyResource {
769
1715
  return Status.FAILURE;
770
1716
  };
771
1717
 
772
- await batch.put(EntryType.INITIALIZATION, safeWriteUInt32BE(0, 0));
1718
+ await batch.put(EntryType.INITIALIZATION, safeWriteUInt32BE(0));
773
1719
  await batch.put(txHashHexString, node.value);
774
1720
 
775
1721
  return Status.SUCCESS;
@@ -782,7 +1728,7 @@ class State extends ReadyResource {
782
1728
  INITIALIZATION, THEIR STAKED BALANCE WILL BE REDUCED (PUNISHMENT).
783
1729
  */
784
1730
 
785
- if (!this.check.validateCoreAdminOperation(op)) {
1731
+ if (!this.#stateValidationSchema.validateCoreAdminOperation(op)) {
786
1732
  this.#safeLogApply(OperationType.ADD_ADMIN, "Contract schema validation failed.", node.from.key)
787
1733
  return Status.FAILURE;
788
1734
  };
@@ -829,9 +1775,9 @@ class State extends ReadyResource {
829
1775
  };
830
1776
 
831
1777
  // verify signature
832
- const isMessageVerifed = tracCryptoApi.signature.verify(op.cao.is, op.cao.tx, adminPublicKey)
1778
+ const isMessageVerified = tracCryptoApi.signature.verify(op.cao.is, op.cao.tx, adminPublicKey)
833
1779
  const txHashHexString = op.cao.tx.toString('hex');
834
- if (!isMessageVerifed) {
1780
+ if (!isMessageVerified) {
835
1781
  this.#safeLogApply(OperationType.ADD_ADMIN, "Failed to verify message signature.", node.from.key)
836
1782
  return Status.FAILURE;
837
1783
  };
@@ -908,7 +1854,7 @@ class State extends ReadyResource {
908
1854
 
909
1855
  // initialize admin entry and initialization flag
910
1856
  await batch.put(EntryType.ADMIN, newAdminEntry);
911
- await batch.put(EntryType.INITIALIZATION, safeWriteUInt32BE(1, 0));
1857
+ await batch.put(EntryType.INITIALIZATION, safeWriteUInt32BE(1));
912
1858
  await batch.put(txHashHexString, node.value);
913
1859
 
914
1860
  if (this.#config.enableTxApplyLogs) {
@@ -919,7 +1865,7 @@ class State extends ReadyResource {
919
1865
  }
920
1866
 
921
1867
  async #handleApplyAdminRecoveryOperation(op, view, base, node, batch) {
922
- if (!this.check.validateRoleAccessOperation(op)) {
1868
+ if (!this.#stateValidationSchema.validateRoleAccessOperation(op)) {
923
1869
  this.#safeLogApply(OperationType.ADMIN_RECOVERY, "Contract schema validation failed.", node.from.key)
924
1870
  return Status.FAILURE;
925
1871
  };
@@ -1170,7 +2116,7 @@ class State extends ReadyResource {
1170
2116
  }
1171
2117
 
1172
2118
  async #handleApplyAppendWhitelistOperation(op, view, base, node, batch) {
1173
- if (!this.check.validateAdminControlOperation(op)) {
2119
+ if (!this.#stateValidationSchema.validateAdminControlOperation(op)) {
1174
2120
  this.#safeLogApply(OperationType.APPEND_WHITELIST, "Contract schema validation failed.", node.from.key)
1175
2121
  return Status.FAILURE;
1176
2122
  };
@@ -1419,7 +2365,7 @@ class State extends ReadyResource {
1419
2365
  }
1420
2366
 
1421
2367
  async #handleApplyAddWriterOperation(op, view, base, node, batch) {
1422
- if (!this.check.validateRoleAccessOperation(op)) {
2368
+ if (!this.#stateValidationSchema.validateRoleAccessOperation(op)) {
1423
2369
  this.#safeLogApply(OperationType.ADD_WRITER, "Contract schema validation failed.", node.from.key)
1424
2370
  return Status.FAILURE;
1425
2371
  };
@@ -1723,7 +2669,7 @@ class State extends ReadyResource {
1723
2669
  }
1724
2670
 
1725
2671
  async #handleApplyRemoveWriterOperation(op, view, base, node, batch) {
1726
- if (!this.check.validateRoleAccessOperation(op)) {
2672
+ if (!this.#stateValidationSchema.validateRoleAccessOperation(op)) {
1727
2673
  this.#safeLogApply(OperationType.REMOVE_WRITER, "Contract schema validation failed.", node.from.key)
1728
2674
  return Status.FAILURE;
1729
2675
  };
@@ -1987,7 +2933,7 @@ class State extends ReadyResource {
1987
2933
  }
1988
2934
 
1989
2935
  async #handleApplyAddIndexerOperation(op, view, base, node, batch) {
1990
- if (!this.check.validateAdminControlOperation(op)) {
2936
+ if (!this.#stateValidationSchema.validateAdminControlOperation(op)) {
1991
2937
  this.#safeLogApply(OperationType.ADD_INDEXER, "Contract schema validation failed.", node.from.key)
1992
2938
  return Status.FAILURE;
1993
2939
  };
@@ -2073,9 +3019,9 @@ class State extends ReadyResource {
2073
3019
  return Status.FAILURE;
2074
3020
  };
2075
3021
 
2076
- const isMessageVerifed = tracCryptoApi.signature.verify(op.aco.is, hash, adminPublicKey);
3022
+ const isMessageVerified = tracCryptoApi.signature.verify(op.aco.is, hash, adminPublicKey);
2077
3023
  const txHashHexString = hash.toString('hex');
2078
- if (!isMessageVerifed) {
3024
+ if (!isMessageVerified) {
2079
3025
  this.#safeLogApply(OperationType.ADD_INDEXER, "Failed to verify message signature.", node.from.key)
2080
3026
  return Status.FAILURE;
2081
3027
  };
@@ -2205,7 +3151,7 @@ class State extends ReadyResource {
2205
3151
  }
2206
3152
 
2207
3153
  async #handleApplyRemoveIndexerOperation(op, view, base, node, batch) {
2208
- if (!this.check.validateAdminControlOperation(op)) {
3154
+ if (!this.#stateValidationSchema.validateAdminControlOperation(op)) {
2209
3155
  this.#safeLogApply(OperationType.REMOVE_INDEXER, "Contract schema validation failed.", node.from.key)
2210
3156
  return Status.FAILURE;
2211
3157
  };
@@ -2288,9 +3234,9 @@ class State extends ReadyResource {
2288
3234
  return Status.FAILURE;
2289
3235
  };
2290
3236
 
2291
- const isMessageVerifed = tracCryptoApi.signature.verify(op.aco.is, hash, adminPublicKey);
3237
+ const isMessageVerified = tracCryptoApi.signature.verify(op.aco.is, hash, adminPublicKey);
2292
3238
  const txHashHexString = hash.toString('hex');
2293
- if (!isMessageVerifed) {
3239
+ if (!isMessageVerified) {
2294
3240
  this.#safeLogApply(OperationType.REMOVE_INDEXER, "Failed to verify message signature.", node.from.key)
2295
3241
  return Status.FAILURE;
2296
3242
  };
@@ -2419,10 +3365,13 @@ class State extends ReadyResource {
2419
3365
  if (this.#config.enableTxApplyLogs) {
2420
3366
  console.info(`Indexer has been removed addr:wk:tx - ${toRemoveAddressString}:${decodedNodeEntry.wk.toString('hex')}:${txHashHexString}`);
2421
3367
  }
3368
+
3369
+ this.#emitEvent(CustomEventType.IS_NON_INDEXER, tracCryptoApi.address.decodeSafe(toRemoveAddressString))
3370
+ return Status.SUCCESS;
2422
3371
  }
2423
3372
 
2424
3373
  async #handleApplyBanValidatorOperation(op, view, base, node, batch) {
2425
- if (!this.check.validateAdminControlOperation(op)) {
3374
+ if (!this.#stateValidationSchema.validateAdminControlOperation(op)) {
2426
3375
  this.#safeLogApply(OperationType.BAN_VALIDATOR, "Contract schema validation failed.", node.from.key)
2427
3376
  return Status.FAILURE;
2428
3377
  };
@@ -2491,9 +3440,9 @@ class State extends ReadyResource {
2491
3440
  return Status.FAILURE;
2492
3441
  };
2493
3442
 
2494
- const isMessageVerifed = tracCryptoApi.signature.verify(op.aco.is, regeneratedHash, adminPublicKey);
3443
+ const isMessageVerified = tracCryptoApi.signature.verify(op.aco.is, regeneratedHash, adminPublicKey);
2495
3444
  const txHashHexString = regeneratedHash.toString('hex');
2496
- if (!isMessageVerifed) {
3445
+ if (!isMessageVerified) {
2497
3446
  this.#safeLogApply(OperationType.BAN_VALIDATOR, "Failed to verify message signature.", node.from.key)
2498
3447
  return Status.FAILURE;
2499
3448
  }
@@ -2623,7 +3572,7 @@ class State extends ReadyResource {
2623
3572
  }
2624
3573
 
2625
3574
  async #handleApplyBootstrapDeploymentOperation(op, view, base, node, batch) {
2626
- if (!this.check.validateBootstrapDeploymentOperation(op)) {
3575
+ if (!this.#stateValidationSchema.validateBootstrapDeploymentOperation(op)) {
2627
3576
  this.#safeLogApply(OperationType.BOOTSTRAP_DEPLOYMENT, "Contract schema validation failed.", node.from.key)
2628
3577
  return Status.FAILURE;
2629
3578
  };
@@ -2857,7 +3806,7 @@ class State extends ReadyResource {
2857
3806
 
2858
3807
  async #handleApplyTxOperation(op, view, base, node, batch) {
2859
3808
  // ATTENTION: The sanitization should be done before ANY other check, otherwise we risk crashing
2860
- if (!this.check.validateTransactionOperation(op)) {
3809
+ if (!this.#stateValidationSchema.validateTransactionOperation(op)) {
2861
3810
  this.#safeLogApply(OperationType.TX, "Contract schema validation failed.", node.from.key)
2862
3811
  return Status.FAILURE;
2863
3812
  };
@@ -3071,7 +4020,7 @@ class State extends ReadyResource {
3071
4020
  }
3072
4021
 
3073
4022
  async #handleApplyTransferOperation(op, view, base, node, batch) {
3074
- if (!this.check.validateTransferOperation(op)) {
4023
+ if (!this.#stateValidationSchema.validateTransferOperation(op)) {
3075
4024
  this.#safeLogApply(OperationType.TRANSFER, "Contract schema validation failed.", node.from.key)
3076
4025
  return Status.FAILURE;
3077
4026
  };
@@ -3108,7 +4057,7 @@ class State extends ReadyResource {
3108
4057
  if (b4a.equals(requesterPublicKey, NULL_BUFFER)) {
3109
4058
  this.#safeLogApply(OperationType.TRANSFER, "Error while decoding requester public key.", node.from.key)
3110
4059
  return Status.FAILURE;
3111
- };
4060
+ }
3112
4061
 
3113
4062
  // recreate requester message
3114
4063
  const requesterMessage = createMessage(
@@ -3266,12 +4215,296 @@ class State extends ReadyResource {
3266
4215
  await batch.put(recipientAddressString, transferResult.recipientEntry);
3267
4216
  }
3268
4217
 
3269
- await batch.put(hashHexString, node.value);
4218
+ await batch.put(hashHexString, node.value);
4219
+
4220
+ if (this.#config.enableTxApplyLogs) {
4221
+ console.info(`Transfer operation: ${hashHexString} has been appended.`);
4222
+ }
4223
+ return Status.SUCCESS;
4224
+ }
4225
+
4226
+ async #handleApplySetEpochOperation(op, _view, base, node, batch) {
4227
+ if (!this.#stateValidationSchema.validateSetEpochOperation(op)) {
4228
+ this.#safeLogApply(OperationType.SET_EPOCH, "Contract schema validation failed.", node.from.key)
4229
+ return Status.FAILURE;
4230
+ }
4231
+
4232
+ const currentConsensusConfigIndexBuffer = await this.#getEntryApply(EntryType.CONSENSUS_CONFIG_CURRENT, batch);
4233
+ if (currentConsensusConfigIndexBuffer === null) {
4234
+ this.#safeLogApply(OperationType.SET_EPOCH, "Consensus config is not initialized.", node.from.key)
4235
+ return Status.FAILURE;
4236
+ }
4237
+
4238
+ if (currentConsensusConfigIndexBuffer.length !== CONSENSUS_CONFIG_INDEX_SIZE) {
4239
+ this.#safeLogApply(OperationType.SET_EPOCH, "Consensus config data is malformed or corrupted.", node.from.key)
4240
+ return Status.FAILURE;
4241
+ }
4242
+
4243
+ const currentConsensusConfigIndex = safeReadUint32BE(currentConsensusConfigIndexBuffer);
4244
+ const consensusConfigBuffer = await this.#getEntryApply(
4245
+ EntryType.CONSENSUS_CONFIG_RECORD + currentConsensusConfigIndex,
4246
+ batch
4247
+ );
4248
+ if (consensusConfigBuffer === null) {
4249
+ this.#safeLogApply(OperationType.SET_EPOCH, "Consensus config record does not exist.", node.from.key)
4250
+ return Status.FAILURE;
4251
+ }
4252
+
4253
+ const consensusConfig = safeDecodeConsensusConfig(consensusConfigBuffer);
4254
+ if (consensusConfig === null) {
4255
+ this.#safeLogApply(OperationType.SET_EPOCH, "Failed to decode consensus config.", node.from.key)
4256
+ return Status.FAILURE;
4257
+ }
4258
+
4259
+ const consensusSchemaVersion = safeReadUint8(consensusConfig.sv);
4260
+ const epochSchemaVersion = safeReadUint8(op.seo.sv);
4261
+ if (consensusSchemaVersion !== epochSchemaVersion) {
4262
+ this.#safeLogApply(OperationType.SET_EPOCH, "Epoch schema version does not match the current consensus config.", node.from.key)
4263
+ return Status.FAILURE;
4264
+ }
4265
+
4266
+ // Use the config at this point in replay, including earlier updates in this batch.
4267
+ switch (consensusSchemaVersion) {
4268
+ case ConsensusVersion.VDF_V1:
4269
+ return await this.#applyVdfV1Epoch(op, consensusConfig, base, node, batch);
4270
+ default:
4271
+ this.#safeLogApply(OperationType.SET_EPOCH, "Unsupported epoch schema version.", node.from.key)
4272
+ return Status.FAILURE;
4273
+ }
4274
+ }
4275
+
4276
+ async #applyVdfV1Epoch(op, consensusConfig, base, node, batch) {
4277
+ const epochProof = safeDecodeEpochProofV1(op.seo.data);
4278
+ if (epochProof === null) {
4279
+ this.#safeLogApply(OperationType.SET_EPOCH, "Failed to decode epoch data.", node.from.key)
4280
+ return Status.FAILURE;
4281
+ }
4282
+ if (!this.#stateValidationSchema.validateEpochProofV1(epochProof)) {
4283
+ this.#safeLogApply(OperationType.SET_EPOCH, "Epoch data schema validation failed.", node.from.key)
4284
+ return Status.FAILURE;
4285
+ }
4286
+
4287
+ const { pd: encodedProofProposal, app: encodedApprovals } = epochProof;
4288
+
4289
+ const requesterAddressBuffer = op.address;
4290
+ const requesterAddressString = addressUtils.bufferToAddress(requesterAddressBuffer, this.#config.addressPrefix);
4291
+ if (requesterAddressString === null) {
4292
+ this.#safeLogApply(OperationType.BOOTSTRAP_DEPLOYMENT, "Requester address is invalid.", node.from.key)
4293
+ return Status.FAILURE;
4294
+ }
4295
+
4296
+ // validate requester public key
4297
+ const requesterPublicKey = tracCryptoApi.address.decodeSafe(requesterAddressString);
4298
+ if (b4a.equals(requesterPublicKey, NULL_BUFFER)) {
4299
+ this.#safeLogApply(OperationType.BOOTSTRAP_DEPLOYMENT, "Failed to decode requester public key.", node.from.key)
4300
+ return Status.FAILURE;
4301
+ }
4302
+
4303
+ const proofProposal = safeDecodeProofProposal(encodedProofProposal);
4304
+ if (proofProposal === null) {
4305
+ this.#safeLogApply(OperationType.SET_EPOCH, "Failed to decode proof proposal.", node.from.key)
4306
+ return Status.FAILURE;
4307
+ }
4308
+
4309
+ const currentEpochBuffer = await this.#getEntryApply(EntryType.EPOCH_CURRENT, batch);
4310
+ if (currentEpochBuffer === null) {
4311
+ this.#safeLogApply(OperationType.SET_EPOCH, "Current epoch is not initialized. Genesis epoch has not been set.", node.from.key)
4312
+ return Status.FAILURE;
4313
+ }
4314
+
4315
+ const nextEpochBuffer = incrementBuffer(currentEpochBuffer, EPOCH_BYTE_LENGTH);
4316
+ if (nextEpochBuffer === null) {
4317
+ this.#safeLogApply(OperationType.SET_EPOCH, "Current epoch index value is invalid or corrupted", node.from.key)
4318
+ return Status.FAILURE;
4319
+ }
4320
+
4321
+ const proposedEpochBuffer = proofProposal.epoch;
4322
+ const epochIndexCompare = b4a.compare(nextEpochBuffer, proposedEpochBuffer)
4323
+
4324
+ // String helpers
4325
+ const currentEpochStr = safeToUIntString(currentEpochBuffer);
4326
+ const nextEpochStr = safeToUIntString(nextEpochBuffer);
4327
+ if (nextEpochStr === null || currentEpochStr === null) {
4328
+ this.#safeLogApply(OperationType.SET_EPOCH, "Epoch index data is malformed or corrupted.", node.from.key)
4329
+ return Status.FAILURE;
4330
+ }
4331
+
4332
+ if (epochIndexCompare > 0) { // proposedEpoch < nextEpoch
4333
+ this.#safeLogApply(OperationType.SET_EPOCH, `Stale epoch proposal. Epoch ${currentEpochStr} is already committed.`, node.from.key)
4334
+ return Status.IGNORE;
4335
+ }
4336
+
4337
+ if (epochIndexCompare < 0) { // proposedEpoch > nextEpoch
4338
+ this.#safeLogApply(OperationType.SET_EPOCH, `Unexpected epoch. Proposal must target epoch ${nextEpochStr} but got ${safeToUIntString(proposedEpochBuffer)}.`, node.from.key)
4339
+ return Status.FAILURE;
4340
+ }
4341
+
4342
+ const expectedNetworkId = uint16ToBuffer(this.#config.networkId);
4343
+ if (!b4a.equals(proofProposal.network_id, expectedNetworkId)) {
4344
+ this.#safeLogApply(OperationType.SET_EPOCH, "Invalid proof proposal network id.", node.from.key)
4345
+ return Status.FAILURE;
4346
+ }
4347
+
4348
+ const currentEpochHash = await this.#getEntryApply(EntryType.EPOCH + currentEpochStr, batch);
4349
+ if (currentEpochHash === null || !b4a.equals(currentEpochHash, proofProposal.previous_epoch_record_hash)) {
4350
+ this.#safeLogApply(OperationType.SET_EPOCH, `Previous epoch record hash mismatch for epoch ${currentEpochStr}.`, node.from.key)
4351
+ return Status.FAILURE;
4352
+ }
4353
+
4354
+ const decodedVdfParams = safeDecodeVdfConfig(consensusConfig.cd);
4355
+ if (decodedVdfParams === null) {
4356
+ this.#safeLogApply(OperationType.SET_EPOCH, "Invalid VDF params value.", node.from.key)
4357
+ return Status.FAILURE;
4358
+ }
4359
+
4360
+ const { difficulty, discriminantBitSize } = decodedVdfParams;
4361
+ if (
4362
+ !b4a.equals(difficulty, proofProposal.difficulty) ||
4363
+ !b4a.equals(discriminantBitSize, proofProposal.discriminant_bit_size)
4364
+ ) {
4365
+ this.#safeLogApply(OperationType.SET_EPOCH, "VDF parameters do not match the current consensus config.", node.from.key)
4366
+ return Status.FAILURE;
4367
+ }
4368
+
4369
+ const indexers = Object.values(base.system.indexers);
4370
+ const indexerAddresses = new Set();
4371
+ for (const indexer of indexers) {
4372
+ const indexerAddressBuffer = await this.#getRegisteredWriterKeyApply(batch, indexer.key.toString('hex'));
4373
+ if (!indexerAddressBuffer) continue;
4374
+ const indexerAddress = addressUtils.bufferToAddress(indexerAddressBuffer, this.#config.addressPrefix);
4375
+ if (indexerAddress) indexerAddresses.add(indexerAddress);
4376
+ }
4377
+
4378
+ const proposerAddress = addressUtils.bufferToAddress(proofProposal.proposer, this.#config.addressPrefix);
4379
+ if (!proposerAddress || !indexerAddresses.has(proposerAddress)) {
4380
+ this.#safeLogApply(OperationType.SET_EPOCH, "Proposer is not a registered indexer.", node.from.key)
4381
+ return Status.FAILURE;
4382
+ }
4383
+
4384
+ const indexerCount = indexers.length;
4385
+ const quorumThreshold = indexerCount <= 2 ? 1 : Math.floor(indexerCount / 2) + 1;
4386
+ const approvalCount = 1 + encodedApprovals.length; // proposer plus submitted approvals (proposer's own verified signature counts as one approval)
4387
+
4388
+ if (approvalCount < quorumThreshold) {
4389
+ this.#safeLogApply(OperationType.SET_EPOCH, `Insufficient submitted approvals for quorum. Required ${quorumThreshold}, got ${approvalCount}.`, node.from.key)
4390
+ return Status.FAILURE;
4391
+ }
4392
+
4393
+ const challengeData = createMessage(
4394
+ proofProposal.network_id,
4395
+ proofProposal.epoch,
4396
+ proofProposal.previous_epoch_record_hash,
4397
+ proofProposal.proposer,
4398
+ proofProposal.difficulty,
4399
+ proofProposal.discriminant_bit_size
4400
+ );
4401
+
4402
+ const proposerPublicKey = tracCryptoApi.address.decodeSafe(proposerAddress);
4403
+ let proposalSignatureVerified = false;
4404
+ if (!b4a.equals(proposerPublicKey, NULL_BUFFER)) {
4405
+ const proposalMessage = createMessage(challengeData, proofProposal.proof);
4406
+ const proposalHash = await tracCryptoApi.hash.blake3Safe(proposalMessage);
4407
+ proposalSignatureVerified = tracCryptoApi.signature.verify(proofProposal.signature, proposalHash, proposerPublicKey);
4408
+ }
4409
+ if (!proposalSignatureVerified) {
4410
+ this.#safeLogApply(OperationType.SET_EPOCH, "Failed to verify proof proposal signature.", node.from.key)
4411
+ return Status.FAILURE;
4412
+ }
4413
+
4414
+ const approvalErrorMessage = await this.#validateEpochApprovals(
4415
+ encodedApprovals,
4416
+ indexerAddresses,
4417
+ proposerAddress,
4418
+ challengeData,
4419
+ proofProposal
4420
+ );
4421
+ if (approvalErrorMessage) {
4422
+ this.#safeLogApply(OperationType.SET_EPOCH, approvalErrorMessage, node.from.key)
4423
+ return Status.FAILURE;
4424
+ }
4425
+
4426
+ let vdfProofVerified = false;
4427
+ try {
4428
+ vdfProofVerified = await verifyWesolowski(
4429
+ challengeData,
4430
+ difficulty.readUInt32BE(0),
4431
+ proofProposal.proof,
4432
+ discriminantBitSize.readUInt16BE(0)
4433
+ );
4434
+ } catch (error) {
4435
+ console.error(error);
4436
+ }
4437
+ if (!vdfProofVerified) {
4438
+ this.#safeLogApply(OperationType.SET_EPOCH, "VDF proof is invalid.", node.from.key)
4439
+ return Status.FAILURE;
4440
+ }
4441
+
4442
+ const encodedEpochProof = safeEncodeEpochProofV1(epochProof);
4443
+ if (encodedEpochProof.length === 0) {
4444
+ this.#safeLogApply(OperationType.SET_EPOCH, "Failed to encode epoch proof.", node.from.key)
4445
+ return Status.FAILURE;
4446
+ }
4447
+
4448
+ const encodedEpochRecord = safeEncodeEpochRecord({ sv: op.seo.sv, data: encodedEpochProof });
4449
+ if (encodedEpochRecord.length === 0) {
4450
+ this.#safeLogApply(OperationType.SET_EPOCH, "Failed to encode epoch record.", node.from.key)
4451
+ return Status.FAILURE;
4452
+ }
4453
+
4454
+ const epochProofHash = await tracCryptoApi.hash.blake3Safe(encodedEpochRecord);
4455
+ if (!isBufferValid(epochProofHash, HASH_BYTE_LENGTH)) {
4456
+ this.#safeLogApply(OperationType.SET_EPOCH, "Failed to hash epoch proof.", node.from.key)
4457
+ return Status.FAILURE;
4458
+ }
4459
+
4460
+ await batch.put(EntryType.EPOCH_CURRENT, nextEpochBuffer);
4461
+ await batch.put(EntryType.EPOCH + nextEpochStr, epochProofHash);
4462
+ await batch.put(EntryType.EPOCH_HASH + epochProofHash.toString('hex'), encodedEpochRecord);
4463
+
4464
+ if (this.#config.enableTxApplyLogs) {
4465
+ 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
4466
+ }
4467
+
4468
+ this.#emitEvent(CustomEventType.EPOCH_CREATED, { epoch: b4a.from(nextEpochBuffer), proposerAddress }); // notify epoch committed
4469
+ return Status.SUCCESS;
4470
+ }
4471
+
4472
+ async #validateEpochApprovals(encodedApprovals, indexerAddresses, proposerAddress, challengeData, proofProposal) {
4473
+ const approverIdentities = new Set();
4474
+
4475
+ for (const encodedApproval of encodedApprovals) {
4476
+ const approval = safeDecodeProofProposalApproval(encodedApproval);
4477
+ if (approval === null) return 'Failed to decode epoch approval.';
4478
+
4479
+ const approverAddress = addressUtils.bufferToAddress(approval.approver, this.#config.addressPrefix);
4480
+ if (!approverAddress) return 'Epoch approval contains an invalid approver.';
4481
+ if (approverAddress === proposerAddress) return 'Proposer cannot approve its own epoch proposal.';
4482
+ if (!indexerAddresses.has(approverAddress)) return 'Epoch approver is not a registered indexer.';
4483
+ if (approverIdentities.has(approverAddress)) return 'Epoch approval contains a duplicate approver.';
4484
+
4485
+ const approverPublicKey = tracCryptoApi.address.decodeSafe(approverAddress);
4486
+ if (b4a.equals(approverPublicKey, NULL_BUFFER)) return 'Failed to decode epoch approver public key.';
4487
+
4488
+ const approvalMessage = createMessage(
4489
+ challengeData,
4490
+ proofProposal.proof,
4491
+ approval.approver,
4492
+ proofProposal.signature
4493
+ );
4494
+ let approvalVerified = false;
4495
+ const approvalHash = await tracCryptoApi.hash.blake3Safe(approvalMessage);
4496
+ approvalVerified = tracCryptoApi.signature.verify(
4497
+ approval.approval_sig,
4498
+ approvalHash,
4499
+ approverPublicKey
4500
+ );
3270
4501
 
3271
- if (this.#config.enableTxApplyLogs) {
3272
- console.info(`Transfer operation: ${hashHexString} has been appended.`);
4502
+ if (!approvalVerified) return 'Failed to verify epoch approval signature.';
4503
+
4504
+ approverIdentities.add(approverAddress);
3273
4505
  }
3274
- return Status.SUCCESS;
4506
+
4507
+ return null;
3275
4508
  }
3276
4509
 
3277
4510
  async #transfer(senderAddressString, recipientAddressString, validatorAddressString, validatorEntryBuffer, transferAmountBuffer, feeAmountBuffer, isSelfTransfer, isRecipientValidator, batch, node) {
@@ -3461,6 +4694,7 @@ class State extends ReadyResource {
3461
4694
  return null
3462
4695
  }
3463
4696
  }
4697
+
3464
4698
  async #isValidatorValidApply(validatorEntryBuffer, node, op) {
3465
4699
  // TODO: Maybe we should transfer all validator checks to this function (address, pubKey, signature, etc)
3466
4700
  if (validatorEntryBuffer === null) {
@@ -3507,7 +4741,7 @@ class State extends ReadyResource {
3507
4741
  if (initialization === null) {
3508
4742
  return false
3509
4743
  } else {
3510
- return b4a.equals(initialization, safeWriteUInt32BE(0, 0))
4744
+ return b4a.equals(initialization, safeWriteUInt32BE(0))
3511
4745
  }
3512
4746
  }
3513
4747
 
@@ -3786,12 +5020,6 @@ class State extends ReadyResource {
3786
5020
  return { newLicenseLength, decodedNewLicenseLength };
3787
5021
  }
3788
5022
 
3789
- #emitEvent(event, ...args) {
3790
- try {
3791
- this.emit(event, ...args)
3792
- } catch (_ignored) { }
3793
- }
3794
-
3795
5023
  async #transferFeeTxOperation(requesterAddressString, validatorAddressString, validatorEntryBuffer, subnetworkCreatorAddressString, feeAmount, batch, node) {
3796
5024
  if (!requesterAddressString ||
3797
5025
  !validatorAddressString ||
@@ -3945,6 +5173,422 @@ class State extends ReadyResource {
3945
5173
  };
3946
5174
  }
3947
5175
 
5176
+ #validateConsensusConfigApply(consensusConfig) {
5177
+ if (!isBufferValid(consensusConfig?.sv, CONSENSUS_CONFIG_SCHEMA_VERSION_BYTE_LENGTH)) {
5178
+ return false;
5179
+ }
5180
+
5181
+ switch (safeReadUint8(consensusConfig.sv)) {
5182
+ case ConsensusVersion.VDF_V1: {
5183
+ const configData = safeDecodeVdfConfig(consensusConfig.cd);
5184
+ if (configData === null) return false;
5185
+
5186
+ return !isZeroBuffer(configData.difficulty) &&
5187
+ Object.hasOwn(VDF_PROOF_BYTE_LENGTHS, configData.discriminantBitSize.readUInt16BE(0));
5188
+ }
5189
+ default:
5190
+ return false;
5191
+ }
5192
+ }
5193
+
5194
+ async #handleApplySetGenesisEpoch(op, view, base, node, batch) {
5195
+ if (!this.#stateValidationSchema.validateConsensusControlOperation(op)) {
5196
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Contract schema validation failed.", node.from.key)
5197
+ return Status.FAILURE;
5198
+ }
5199
+
5200
+ // Extract and validate the requester address (admin)
5201
+ const requesterAddressBuffer = op.address;
5202
+ const requesterAddressString = addressUtils.bufferToAddress(requesterAddressBuffer, this.#config.addressPrefix);
5203
+ if (requesterAddressString === null) {
5204
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Requester address is invalid.", node.from.key)
5205
+ return Status.FAILURE;
5206
+ }
5207
+
5208
+ // Validate requester public key
5209
+ const requesterPublicKey = tracCryptoApi.address.decodeSafe(requesterAddressString);
5210
+ if (b4a.equals(requesterPublicKey, NULL_BUFFER)) {
5211
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Failed to decode requester public key.", node.from.key)
5212
+ return Status.FAILURE;
5213
+ }
5214
+ // ensure that an admin invoked this operation
5215
+ const adminEntry = await this.#getEntryApply(EntryType.ADMIN, batch);
5216
+ if (adminEntry === null) {
5217
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Invalid admin entry.", node.from.key)
5218
+ return Status.FAILURE;
5219
+ }
5220
+
5221
+ const decodedAdminEntry = adminEntryUtils.decode(adminEntry, this.#config.addressPrefix);
5222
+ if (decodedAdminEntry === null) {
5223
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Failed to decode admin entry.", node.from.key)
5224
+ return Status.FAILURE;
5225
+ }
5226
+
5227
+ if (!this.#isAdminApply(decodedAdminEntry, node)) {
5228
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Node is not allowed to perform this operation. (ADMIN ONLY)", node.from.key)
5229
+ return Status.FAILURE;
5230
+ }
5231
+
5232
+ // Extract admin public key
5233
+ const adminPublicKey = tracCryptoApi.address.decodeSafe(decodedAdminEntry.address);
5234
+ if (b4a.equals(adminPublicKey, NULL_BUFFER)) {
5235
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Failed to decode admin public key.", node.from.key)
5236
+ return Status.FAILURE;
5237
+ }
5238
+ // Admin consistency check
5239
+ if (!b4a.equals(adminPublicKey, requesterPublicKey)) {
5240
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "System admin and node public keys do not match.", node.from.key)
5241
+ return Status.FAILURE;
5242
+ }
5243
+
5244
+ const encodedConsensusConfig = safeEncodeConsensusConfig(op.cco.cc);
5245
+
5246
+ if (encodedConsensusConfig.length === 0) {
5247
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Failed to encode consensus config.", node.from.key);
5248
+ return Status.FAILURE;
5249
+ }
5250
+
5251
+ if (!this.#validateConsensusConfigApply(op.cco.cc)) {
5252
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Consensus config validation failed.", node.from.key);
5253
+ return Status.FAILURE;
5254
+ }
5255
+
5256
+ // verify requester signature
5257
+ const message = createMessage(
5258
+ this.#config.networkId,
5259
+ op.cco.txv,
5260
+ encodedConsensusConfig,
5261
+ op.cco.in,
5262
+ OperationType.SET_GENESIS_EPOCH
5263
+ );
5264
+
5265
+ if (message.length === 0) {
5266
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Invalid requester message.", node.from.key)
5267
+ return Status.FAILURE;
5268
+ }
5269
+
5270
+ const hash = await tracCryptoApi.hash.blake3Safe(message);
5271
+ if (!b4a.equals(hash, op.cco.tx)) {
5272
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Message hash does not match the tx_hash.", node.from.key)
5273
+ return Status.FAILURE;
5274
+ }
5275
+
5276
+ // verify signature
5277
+ const isMessageVerified = tracCryptoApi.signature.verify(op.cco.is, op.cco.tx, adminPublicKey)
5278
+ const txHashHexString = op.cco.tx.toString('hex');
5279
+
5280
+ if (!isMessageVerified) {
5281
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Failed to verify message signature.", node.from.key)
5282
+ return Status.FAILURE;
5283
+ }
5284
+
5285
+ // verify tx validity - prevent deferred execution attack
5286
+ const indexersSequenceState = await this.#getIndexerSequenceStateApply(base);
5287
+ if (indexersSequenceState === null) {
5288
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Indexer sequence state is invalid.", node.from.key)
5289
+ return Status.FAILURE;
5290
+ }
5291
+
5292
+ if (!b4a.equals(op.cco.txv, indexersSequenceState)) {
5293
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Transaction was not executed.", node.from.key)
5294
+ return Status.FAILURE;
5295
+ }
5296
+
5297
+ // anti-replay attack
5298
+ const opEntry = await this.#getEntryApply(txHashHexString, batch);
5299
+ if (opEntry !== null) {
5300
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Operation has already been applied.", node.from.key)
5301
+ return Status.IGNORE;
5302
+ }
5303
+
5304
+ // check if CurrentEpoch have been initialized if yes - failure
5305
+ const currentEpoch = await this.#getEntryApply(EntryType.EPOCH_CURRENT, batch);
5306
+ if (currentEpoch !== null) {
5307
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Current epoch is set. Cannot set a new genesis epoch", node.from.key)
5308
+ return Status.IGNORE;
5309
+ }
5310
+
5311
+ // check if genesis epoch is initialized. If yes - failure
5312
+ const epochZero = EntryType.EPOCH + "0";
5313
+ const genesisEpochHash = await this.#getEntryApply(epochZero , batch);
5314
+ if (genesisEpochHash !== null) {
5315
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Genesis epoch is set. Cannot set a new one", node.from.key)
5316
+ return Status.IGNORE;
5317
+ }
5318
+
5319
+ const currentConsensusConfigIndex = await this.#getEntryApply(
5320
+ EntryType.CONSENSUS_CONFIG_CURRENT,
5321
+ batch
5322
+ );
5323
+
5324
+ const genesisConsensusConfigKey = EntryType.CONSENSUS_CONFIG_RECORD + 0;
5325
+
5326
+ const genesisConsensusConfig = await this.#getEntryApply(
5327
+ genesisConsensusConfigKey,
5328
+ batch
5329
+ );
5330
+
5331
+ // Check if currently genesis config exists
5332
+ if (currentConsensusConfigIndex !== null || genesisConsensusConfig !== null) {
5333
+ this.#safeLogApply(
5334
+ OperationType.SET_GENESIS_EPOCH,
5335
+ "Genesis consensus config is set. Cannot set a new genesis epoch",
5336
+ node.from.key
5337
+ );
5338
+ return Status.IGNORE;
5339
+ }
5340
+
5341
+
5342
+ const genesisEpoch = await createGenesisEpochProof(
5343
+ requesterAddressString,
5344
+ encodedConsensusConfig,
5345
+ this.#config
5346
+ );
5347
+
5348
+ if (!b4a.isBuffer(genesisEpoch) || genesisEpoch.length === 0) {
5349
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Could not initialize genesis epoch", node.from.key)
5350
+ return Status.FAILURE;
5351
+ }
5352
+
5353
+ const epochProofHash = await tracCryptoApi.hash.blake3Safe(genesisEpoch);
5354
+ if (!isBufferValid(epochProofHash, HASH_BYTE_LENGTH)) {
5355
+ this.#safeLogApply(OperationType.SET_GENESIS_EPOCH, "Failed to hash genesis epoch proof.", node.from.key)
5356
+ return Status.FAILURE;
5357
+ }
5358
+
5359
+ // initialize CurrentEpoch field
5360
+ const zeroAsUint64Buffer = b4a.alloc(8, 0);
5361
+ await batch.put(
5362
+ EntryType.EPOCH_CURRENT,
5363
+ zeroAsUint64Buffer
5364
+ );
5365
+
5366
+ // initialize Epoch Field
5367
+ await batch.put(
5368
+ epochZero,
5369
+ epochProofHash
5370
+ );
5371
+
5372
+ // initialize EpochHash Field
5373
+ const epochProofHashString = epochProofHash.toString('hex');
5374
+ const epochHashLedgerEntry = EntryType.EPOCH_HASH + epochProofHashString;
5375
+ await batch.put(
5376
+ epochHashLedgerEntry,
5377
+ genesisEpoch
5378
+ );
5379
+
5380
+ // Record 0 preserves the initial config, including the genesis format version.
5381
+ await batch.put(
5382
+ EntryType.CONSENSUS_CONFIG_CURRENT,
5383
+ safeWriteUInt32BE(0)
5384
+ );
5385
+ await batch.put(
5386
+ genesisConsensusConfigKey,
5387
+ encodedConsensusConfig
5388
+ );
5389
+
5390
+ // Put txHashHexString into the state to avoid replay attack
5391
+ await batch.put(txHashHexString, node.value);
5392
+
5393
+ if (this.#config.enableTxApplyLogs) {
5394
+ console.info(`Genesis Epoch initialized addr:wk:tx - ${requesterAddressString}:${decodedAdminEntry.wk.toString('hex')}:${txHashHexString}`);
5395
+ }
5396
+
5397
+ this.#emitEvent(CustomEventType.GENESIS_EPOCH_CREATED, { epoch: 0n, proposerAddress: requesterAddressString });
5398
+ return Status.SUCCESS;
5399
+ }
5400
+
5401
+ async #handleApplySetConsensusConfig(op, _view, base, node, batch, postApplyEvents) {
5402
+ if (!this.#stateValidationSchema.validateConsensusControlOperation(op)) {
5403
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Contract schema validation failed.", node.from.key)
5404
+ return Status.FAILURE;
5405
+ }
5406
+
5407
+ const requesterAddressString = addressUtils.bufferToAddress(op.address, this.#config.addressPrefix);
5408
+ if (requesterAddressString === null) {
5409
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Requester address is invalid.", node.from.key)
5410
+ return Status.FAILURE;
5411
+ }
5412
+
5413
+ const requesterPublicKey = tracCryptoApi.address.decodeSafe(requesterAddressString);
5414
+ if (b4a.equals(requesterPublicKey, NULL_BUFFER)) {
5415
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Failed to decode requester public key.", node.from.key)
5416
+ return Status.FAILURE;
5417
+ }
5418
+
5419
+ const adminEntry = await this.#getEntryApply(EntryType.ADMIN, batch);
5420
+ if (adminEntry === null) {
5421
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Invalid admin entry.", node.from.key)
5422
+ return Status.FAILURE;
5423
+ }
5424
+
5425
+ const decodedAdminEntry = adminEntryUtils.decode(adminEntry, this.#config.addressPrefix);
5426
+ if (decodedAdminEntry === null) {
5427
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Failed to decode admin entry.", node.from.key)
5428
+ return Status.FAILURE;
5429
+ }
5430
+
5431
+ if (!this.#isAdminApply(decodedAdminEntry, node)) {
5432
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Node is not allowed to perform this operation. (ADMIN ONLY)", node.from.key)
5433
+ return Status.FAILURE;
5434
+ }
5435
+
5436
+ const adminPublicKey = tracCryptoApi.address.decodeSafe(decodedAdminEntry.address);
5437
+ if (b4a.equals(adminPublicKey, NULL_BUFFER)) {
5438
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Failed to decode admin public key.", node.from.key)
5439
+ return Status.FAILURE;
5440
+ }
5441
+
5442
+ if (!b4a.equals(adminPublicKey, requesterPublicKey)) {
5443
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "System admin and node public keys do not match.", node.from.key)
5444
+ return Status.FAILURE;
5445
+ }
5446
+
5447
+ const encodedConsensusConfig = safeEncodeConsensusConfig(op.cco.cc);
5448
+
5449
+ if (encodedConsensusConfig.length === 0) {
5450
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Failed to encode consensus config.", node.from.key);
5451
+ return Status.FAILURE;
5452
+ }
5453
+
5454
+ if (!this.#validateConsensusConfigApply(op.cco.cc)) {
5455
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Consensus config validation failed.", node.from.key);
5456
+ return Status.FAILURE;
5457
+ }
5458
+
5459
+ const message = createMessage(
5460
+ this.#config.networkId,
5461
+ op.cco.txv,
5462
+ encodedConsensusConfig,
5463
+ op.cco.in,
5464
+ OperationType.SET_CONSENSUS_CONFIG
5465
+ );
5466
+
5467
+ if (message.length === 0) {
5468
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Invalid requester message.", node.from.key)
5469
+ return Status.FAILURE;
5470
+ }
5471
+
5472
+ const hash = await tracCryptoApi.hash.blake3Safe(message);
5473
+ if (!b4a.equals(hash, op.cco.tx)) {
5474
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Message hash does not match the tx_hash.", node.from.key)
5475
+ return Status.FAILURE;
5476
+ }
5477
+
5478
+ const isMessageVerified = tracCryptoApi.signature.verify(op.cco.is, op.cco.tx, adminPublicKey);
5479
+ if (!isMessageVerified) {
5480
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Failed to verify message signature.", node.from.key)
5481
+ return Status.FAILURE;
5482
+ }
5483
+
5484
+ const indexersSequenceState = await this.#getIndexerSequenceStateApply(base);
5485
+ if (indexersSequenceState === null) {
5486
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Indexer sequence state is invalid.", node.from.key)
5487
+ return Status.FAILURE;
5488
+ }
5489
+
5490
+ if (!b4a.equals(op.cco.txv, indexersSequenceState)) {
5491
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Transaction was not executed.", node.from.key)
5492
+ return Status.FAILURE;
5493
+ }
5494
+
5495
+ const txHashHexString = op.cco.tx.toString('hex');
5496
+ const opEntry = await this.#getEntryApply(txHashHexString, batch);
5497
+ if (opEntry !== null) {
5498
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Operation has already been applied.", node.from.key)
5499
+ return Status.IGNORE;
5500
+ }
5501
+
5502
+ const currentConsensusConfigIndexBuffer = await this.#getEntryApply(EntryType.CONSENSUS_CONFIG_CURRENT, batch);
5503
+ if (currentConsensusConfigIndexBuffer === null) {
5504
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Initial consensus config has not been initialized yet", node.from.key)
5505
+ return Status.IGNORE;
5506
+ }
5507
+
5508
+ if (currentConsensusConfigIndexBuffer.length !== CONSENSUS_CONFIG_INDEX_SIZE) {
5509
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Consensus config index is malformed or corrupted.", node.from.key)
5510
+ return Status.FAILURE;
5511
+ }
5512
+
5513
+ const currentConsensusConfigIndex = safeReadUint32BE(currentConsensusConfigIndexBuffer);
5514
+ if (currentConsensusConfigIndex === null) {
5515
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG,"Failed to read current consensus config index from buffer", node.from.key)
5516
+ return Status.FAILURE;
5517
+ }
5518
+ if (currentConsensusConfigIndex === UINT32_MAX) {
5519
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Consensus config index overflow.", node.from.key)
5520
+ return Status.FAILURE;
5521
+ }
5522
+
5523
+ const currentConsensusConfigBuffer = await this.#getEntryApply(
5524
+ EntryType.CONSENSUS_CONFIG_RECORD + currentConsensusConfigIndex,
5525
+ batch
5526
+ );
5527
+ const currentConsensusConfig = safeDecodeConsensusConfig(currentConsensusConfigBuffer);
5528
+ if (currentConsensusConfig === null) {
5529
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Failed to decode current consensus config.", node.from.key)
5530
+ return Status.FAILURE;
5531
+ }
5532
+
5533
+ const currentSchemaVersion = safeReadUint8(currentConsensusConfig.sv);
5534
+ const nextSchemaVersion = safeReadUint8(op.cco.cc.sv);
5535
+ if (currentSchemaVersion === null || currentSchemaVersion === 0) {
5536
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Current consensus config schema version is invalid.", node.from.key)
5537
+ return Status.FAILURE;
5538
+ }
5539
+
5540
+ // Same-version parameter changes are allowed; returning to an older consensus is not.
5541
+ if (nextSchemaVersion < currentSchemaVersion) {
5542
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Consensus config schema version cannot decrease.", node.from.key)
5543
+ return Status.FAILURE;
5544
+ }
5545
+
5546
+ const nextConsensusConfigIndex = currentConsensusConfigIndex + 1;
5547
+ const nextConsensusConfigKey = EntryType.CONSENSUS_CONFIG_RECORD + nextConsensusConfigIndex;
5548
+ const nextConsensusConfigIndexBuffer = safeWriteUInt32BE(nextConsensusConfigIndex);
5549
+
5550
+ if (nextConsensusConfigIndexBuffer.length === 0) {
5551
+ this.#safeLogApply(OperationType.SET_CONSENSUS_CONFIG, "Failed to encode next consensus config index.", node.from.key);
5552
+ return Status.FAILURE;
5553
+ }
5554
+
5555
+ await batch.put(EntryType.CONSENSUS_CONFIG_CURRENT, nextConsensusConfigIndexBuffer);
5556
+ await batch.put(nextConsensusConfigKey, encodedConsensusConfig);
5557
+ await batch.put(txHashHexString, node.value);
5558
+
5559
+ if (this.#config.enableTxApplyLogs) {
5560
+ console.info(`Consensus config updated addr:wk:tx - ${requesterAddressString}:${decodedAdminEntry.wk.toString('hex')}:${txHashHexString}`);
5561
+ }
5562
+
5563
+ postApplyEvents.push({
5564
+ type: CustomEventType.CONSENSUS_CONFIG_CHANGED,
5565
+ });
5566
+
5567
+ return Status.SUCCESS;
5568
+ }
5569
+
5570
+ async getUnsignedConsensusConfig() {
5571
+ // Keep the pointer and record in the same view while unsigned state can change.
5572
+ const snapshot = this.#base.view.snapshot();
5573
+ try {
5574
+ const currentConfigPointer = (await snapshot.get(EntryType.CONSENSUS_CONFIG_CURRENT))?.value;
5575
+ if (_.isNil(currentConfigPointer)) return null;
5576
+
5577
+ if (!b4a.isBuffer(currentConfigPointer) || currentConfigPointer.length !== CONSENSUS_CONFIG_INDEX_SIZE) {
5578
+ throw new Error('Invalid current consensus config pointer.');
5579
+ }
5580
+
5581
+ const currentConfigIndex = currentConfigPointer.readUInt32BE(0);
5582
+ const encodedConsensusConfig = (await snapshot.get(EntryType.CONSENSUS_CONFIG_RECORD + currentConfigIndex))?.value;
5583
+ if (_.isNil(encodedConsensusConfig)) {
5584
+ throw new Error(`Consensus config record ${currentConfigIndex} does not exist.`);
5585
+ }
5586
+
5587
+ return decodeVersionedConsensusConfig(encodedConsensusConfig);
5588
+ } finally {
5589
+ await snapshot.close();
5590
+ }
5591
+ }
3948
5592
  }
3949
5593
 
3950
5594
  export default State;