@lodestar/validator 1.43.0-dev.a140dd987e → 1.43.0-dev.a45ba75824

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 (41) hide show
  1. package/lib/metrics.d.ts +10 -0
  2. package/lib/metrics.d.ts.map +1 -1
  3. package/lib/metrics.js +37 -0
  4. package/lib/metrics.js.map +1 -1
  5. package/lib/services/chainHeaderTracker.d.ts +8 -2
  6. package/lib/services/chainHeaderTracker.d.ts.map +1 -1
  7. package/lib/services/chainHeaderTracker.js +23 -8
  8. package/lib/services/chainHeaderTracker.js.map +1 -1
  9. package/lib/services/emitter.d.ts +14 -1
  10. package/lib/services/emitter.d.ts.map +1 -1
  11. package/lib/services/emitter.js +22 -0
  12. package/lib/services/emitter.js.map +1 -1
  13. package/lib/services/ptc.d.ts +28 -0
  14. package/lib/services/ptc.d.ts.map +1 -0
  15. package/lib/services/ptc.js +89 -0
  16. package/lib/services/ptc.js.map +1 -0
  17. package/lib/services/ptcDuties.d.ts +31 -0
  18. package/lib/services/ptcDuties.d.ts.map +1 -0
  19. package/lib/services/ptcDuties.js +201 -0
  20. package/lib/services/ptcDuties.js.map +1 -0
  21. package/lib/services/validatorStore.d.ts +2 -0
  22. package/lib/services/validatorStore.d.ts.map +1 -1
  23. package/lib/services/validatorStore.js +30 -1
  24. package/lib/services/validatorStore.js.map +1 -1
  25. package/lib/util/externalSignerClient.d.ts +5 -1
  26. package/lib/util/externalSignerClient.d.ts.map +1 -1
  27. package/lib/util/externalSignerClient.js +4 -0
  28. package/lib/util/externalSignerClient.js.map +1 -1
  29. package/lib/validator.d.ts +4 -1
  30. package/lib/validator.d.ts.map +1 -1
  31. package/lib/validator.js +8 -2
  32. package/lib/validator.js.map +1 -1
  33. package/package.json +12 -12
  34. package/src/metrics.ts +46 -0
  35. package/src/services/chainHeaderTracker.ts +31 -7
  36. package/src/services/emitter.ts +31 -0
  37. package/src/services/ptc.ts +131 -0
  38. package/src/services/ptcDuties.ts +246 -0
  39. package/src/services/validatorStore.ts +42 -0
  40. package/src/util/externalSignerClient.ts +7 -1
  41. package/src/validator.ts +20 -1
@@ -0,0 +1,246 @@
1
+ import {ApiClient, routes} from "@lodestar/api";
2
+ import {ChainForkConfig} from "@lodestar/config";
3
+ import {SLOTS_PER_EPOCH, isForkPostGloas} from "@lodestar/params";
4
+ import {computeEpochAtSlot, isStartSlotOfEpoch} from "@lodestar/state-transition";
5
+ import {Epoch, RootHex, Slot, ValidatorIndex} from "@lodestar/types";
6
+ import {toPubkeyHex} from "@lodestar/utils";
7
+ import {Metrics} from "../metrics.js";
8
+ import {PubkeyHex} from "../types.js";
9
+ import {IClock, LoggerVc} from "../util/index.js";
10
+ import {ChainHeaderTracker, HeadEventData} from "./chainHeaderTracker.js";
11
+ import {SyncingStatusTracker} from "./syncingStatusTracker.js";
12
+ import {ValidatorStore} from "./validatorStore.js";
13
+
14
+ /** Only retain `HISTORICAL_DUTIES_EPOCHS` duties prior to the current epoch. */
15
+ const HISTORICAL_DUTIES_EPOCHS = 2;
16
+
17
+ type PtcDutiesAtEpoch = {dependentRoot: RootHex; dutiesByIndex: Map<ValidatorIndex, routes.validator.PtcDuty>};
18
+
19
+ export class PtcDutiesService {
20
+ /** Maps a validator index to its PTC duty for each epoch. */
21
+ private readonly dutiesByIndexByEpoch = new Map<Epoch, PtcDutiesAtEpoch>();
22
+
23
+ constructor(
24
+ private readonly config: ChainForkConfig,
25
+ private readonly logger: LoggerVc,
26
+ private readonly api: ApiClient,
27
+ private readonly clock: IClock,
28
+ private readonly validatorStore: ValidatorStore,
29
+ chainHeadTracker: ChainHeaderTracker,
30
+ syncingStatusTracker: SyncingStatusTracker,
31
+ private readonly metrics: Metrics | null
32
+ ) {
33
+ clock.runEveryEpoch(this.runDutiesTasks);
34
+ chainHeadTracker.runOnNewHead(this.onNewHead);
35
+ syncingStatusTracker.runOnResynced(async (slot) => {
36
+ // Skip on first slot of epoch since tasks are already scheduled.
37
+ if (!isStartSlotOfEpoch(slot)) {
38
+ return this.runDutiesTasks(computeEpochAtSlot(slot));
39
+ }
40
+ });
41
+
42
+ if (metrics) {
43
+ metrics.ptcDutiesCount.addCollect(() => {
44
+ const currentSlot = this.clock.getCurrentSlot();
45
+ let duties = 0;
46
+ let nextDutySlot = null;
47
+ for (const [epoch, ptcDutiesAtEpoch] of this.dutiesByIndexByEpoch) {
48
+ duties += ptcDutiesAtEpoch.dutiesByIndex.size;
49
+
50
+ // Epochs are sorted, stop searching once a next duty slot is found.
51
+ if (epoch < this.clock.currentEpoch || nextDutySlot !== null) continue;
52
+
53
+ for (const duty of ptcDutiesAtEpoch.dutiesByIndex.values()) {
54
+ if (duty.slot > currentSlot && (nextDutySlot === null || duty.slot < nextDutySlot)) {
55
+ nextDutySlot = duty.slot;
56
+ }
57
+ }
58
+ }
59
+ metrics.ptcDutiesCount.set(duties);
60
+ metrics.ptcDutiesEpochCount.set(this.dutiesByIndexByEpoch.size);
61
+ if (nextDutySlot !== null) metrics.ptcDutiesNextSlot.set(nextDutySlot);
62
+ });
63
+ }
64
+ }
65
+
66
+ removeDutiesForKey(pubkey: PubkeyHex): void {
67
+ for (const [epoch, ptcDutiesAtEpoch] of this.dutiesByIndexByEpoch) {
68
+ for (const [validatorIndex, duty] of ptcDutiesAtEpoch.dutiesByIndex) {
69
+ if (toPubkeyHex(duty.pubkey) === pubkey) {
70
+ ptcDutiesAtEpoch.dutiesByIndex.delete(validatorIndex);
71
+ if (ptcDutiesAtEpoch.dutiesByIndex.size === 0) {
72
+ this.dutiesByIndexByEpoch.delete(epoch);
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ /** Returns all PTC duties for the given slot. */
80
+ getDutiesAtSlot(slot: Slot): routes.validator.PtcDuty[] {
81
+ const epoch = computeEpochAtSlot(slot);
82
+ const duties: routes.validator.PtcDuty[] = [];
83
+ const epochDuties = this.dutiesByIndexByEpoch.get(epoch);
84
+ if (epochDuties === undefined) {
85
+ return duties;
86
+ }
87
+
88
+ for (const duty of epochDuties.dutiesByIndex.values()) {
89
+ if (duty.slot === slot) {
90
+ duties.push(duty);
91
+ }
92
+ }
93
+
94
+ return duties;
95
+ }
96
+
97
+ private runDutiesTasks = async (epoch: Epoch): Promise<void> => {
98
+ const nextEpoch = epoch + 1;
99
+ if (!isForkPostGloas(this.config.getForkName(nextEpoch * SLOTS_PER_EPOCH))) {
100
+ return;
101
+ }
102
+
103
+ await Promise.all([
104
+ this.pollPtcDuties(epoch, this.validatorStore.getAllLocalIndices()).catch((e: Error) => {
105
+ this.logger.error("Error on poll PTC duties", {epoch}, e);
106
+ }),
107
+
108
+ this.validatorStore
109
+ .pollValidatorIndices()
110
+ .then((newIndices) => this.pollPtcDuties(epoch, newIndices))
111
+ .catch((e: Error) => {
112
+ this.logger.error("Error on poll indices and PTC duties", {epoch}, e);
113
+ }),
114
+ ]);
115
+
116
+ this.pruneOldDuties(epoch);
117
+ };
118
+
119
+ private async pollPtcDuties(currentEpoch: Epoch, indexArr: ValidatorIndex[]): Promise<void> {
120
+ const nextEpoch = currentEpoch + 1;
121
+
122
+ if (indexArr.length === 0) {
123
+ return;
124
+ }
125
+
126
+ for (const epoch of [currentEpoch, nextEpoch]) {
127
+ await this.pollPtcDutiesForEpoch(epoch, indexArr).catch((e: Error) => {
128
+ this.logger.error("Failed to download PTC duties", {epoch}, e);
129
+ });
130
+ }
131
+ }
132
+
133
+ private async pollPtcDutiesForEpoch(epoch: Epoch, indexArr: ValidatorIndex[]): Promise<void> {
134
+ if (epoch < 0) {
135
+ return;
136
+ }
137
+
138
+ if (!isForkPostGloas(this.config.getForkName(epoch * SLOTS_PER_EPOCH))) {
139
+ return;
140
+ }
141
+
142
+ const res = await this.api.validator.getPtcDuties({epoch, indices: indexArr});
143
+ const ptcDuties = res.value();
144
+ const {dependentRoot} = res.meta();
145
+ const relevantDuties = ptcDuties.filter((duty) => {
146
+ const pubkeyHex = toPubkeyHex(duty.pubkey);
147
+ return this.validatorStore.hasVotingPubkey(pubkeyHex) && this.validatorStore.isDoppelgangerSafe(pubkeyHex);
148
+ });
149
+
150
+ this.logger.debug("Downloaded PTC duties", {epoch, dependentRoot, count: relevantDuties.length});
151
+
152
+ const dutiesAtEpoch = this.dutiesByIndexByEpoch.get(epoch);
153
+ const priorDependentRoot = dutiesAtEpoch?.dependentRoot;
154
+ const dependentRootChanged = priorDependentRoot !== undefined && priorDependentRoot !== dependentRoot;
155
+
156
+ if (!priorDependentRoot || dependentRootChanged) {
157
+ const dutiesByIndex = new Map<ValidatorIndex, routes.validator.PtcDuty>();
158
+ for (const duty of relevantDuties) {
159
+ dutiesByIndex.set(duty.validatorIndex, duty);
160
+ }
161
+ this.dutiesByIndexByEpoch.set(epoch, {dependentRoot, dutiesByIndex});
162
+
163
+ if (priorDependentRoot && dependentRootChanged) {
164
+ this.metrics?.ptcDutiesReorg.inc();
165
+ this.logger.warn("PTC duties re-org. This may happen from time to time", {
166
+ priorDependentRoot,
167
+ dependentRoot,
168
+ epoch,
169
+ });
170
+ }
171
+ } else {
172
+ const existingDuties = dutiesAtEpoch.dutiesByIndex;
173
+ const existingDutiesCount = existingDuties.size;
174
+ const discoveredNewDuties = relevantDuties.length > existingDutiesCount;
175
+
176
+ if (discoveredNewDuties) {
177
+ for (const duty of relevantDuties) {
178
+ if (!existingDuties.has(duty.validatorIndex)) {
179
+ existingDuties.set(duty.validatorIndex, duty);
180
+ }
181
+ }
182
+
183
+ this.logger.debug("Discovered new PTC duties", {
184
+ epoch,
185
+ dependentRoot,
186
+ count: relevantDuties.length - existingDutiesCount,
187
+ });
188
+ }
189
+ }
190
+ }
191
+
192
+ private onNewHead = async ({
193
+ slot,
194
+ previousDutyDependentRoot,
195
+ currentDutyDependentRoot,
196
+ }: HeadEventData): Promise<void> => {
197
+ const currentEpoch = computeEpochAtSlot(slot);
198
+ const nextEpoch = currentEpoch + 1;
199
+
200
+ const nextEpochDependentRoot = this.dutiesByIndexByEpoch.get(nextEpoch)?.dependentRoot;
201
+ if (nextEpochDependentRoot && currentDutyDependentRoot !== nextEpochDependentRoot) {
202
+ this.logger.warn("Potential next epoch PTC duties reorg", {
203
+ slot,
204
+ dutyEpoch: nextEpoch,
205
+ priorDependentRoot: nextEpochDependentRoot,
206
+ newDependentRoot: currentDutyDependentRoot,
207
+ });
208
+ await this.handlePtcDutiesReorg(nextEpoch, slot, nextEpochDependentRoot, currentDutyDependentRoot);
209
+ }
210
+
211
+ const currentEpochDependentRoot = this.dutiesByIndexByEpoch.get(currentEpoch)?.dependentRoot;
212
+ if (currentEpochDependentRoot && currentEpochDependentRoot !== previousDutyDependentRoot) {
213
+ this.logger.warn("Potential current epoch PTC duties reorg", {
214
+ slot,
215
+ dutyEpoch: currentEpoch,
216
+ priorDependentRoot: currentEpochDependentRoot,
217
+ newDependentRoot: previousDutyDependentRoot,
218
+ });
219
+ await this.handlePtcDutiesReorg(currentEpoch, slot, currentEpochDependentRoot, previousDutyDependentRoot);
220
+ }
221
+ };
222
+
223
+ private async handlePtcDutiesReorg(
224
+ dutyEpoch: Epoch,
225
+ slot: Slot,
226
+ oldDependentRoot: RootHex,
227
+ newDependentRoot: RootHex
228
+ ): Promise<void> {
229
+ this.metrics?.ptcDutiesReorg.inc();
230
+ const logContext = {dutyEpoch, slot, oldDependentRoot, newDependentRoot};
231
+ this.logger.debug("Redownload PTC duties", logContext);
232
+
233
+ await this.pollPtcDutiesForEpoch(dutyEpoch, this.validatorStore.getAllLocalIndices()).catch((e: Error) => {
234
+ this.logger.error("Failed to redownload PTC duties when reorg happens", logContext, e);
235
+ });
236
+ }
237
+
238
+ /** Run once per epoch to prune duties map. */
239
+ private pruneOldDuties(currentEpoch: Epoch): void {
240
+ for (const epoch of this.dutiesByIndexByEpoch.keys()) {
241
+ if (epoch + HISTORICAL_DUTIES_EPOCHS < currentEpoch) {
242
+ this.dutiesByIndexByEpoch.delete(epoch);
243
+ }
244
+ }
245
+ }
246
+ }
@@ -9,6 +9,7 @@ import {
9
9
  DOMAIN_BEACON_BUILDER,
10
10
  DOMAIN_BEACON_PROPOSER,
11
11
  DOMAIN_CONTRIBUTION_AND_PROOF,
12
+ DOMAIN_PTC_ATTESTER,
12
13
  DOMAIN_RANDAO,
13
14
  DOMAIN_SELECTION_PROOF,
14
15
  DOMAIN_SYNC_COMMITTEE,
@@ -668,6 +669,41 @@ export class ValidatorStore {
668
669
  };
669
670
  }
670
671
 
672
+ async signPayloadAttestation(
673
+ duty: routes.validator.PtcDuty,
674
+ data: gloas.PayloadAttestationData,
675
+ currentSlot: Slot,
676
+ logger?: LoggerVc
677
+ ): Promise<gloas.PayloadAttestationMessage> {
678
+ if (data.slot > currentSlot) {
679
+ throw Error(`Not signing payload attestation with slot ${data.slot} greater than current slot ${currentSlot}`);
680
+ }
681
+
682
+ this.assertDoppelgangerSafe(duty.pubkey);
683
+ this.validatePtcDuty(duty, data);
684
+
685
+ const signingSlot = data.slot;
686
+ const domain = this.config.getDomain(signingSlot, DOMAIN_PTC_ATTESTER);
687
+ const signingRoot = computeSigningRoot(ssz.gloas.PayloadAttestationData, data, domain);
688
+
689
+ logger?.debug("Signing payload attestation message", {
690
+ slot: signingSlot,
691
+ beaconBlockRoot: toRootHex(data.beaconBlockRoot),
692
+ signingRoot: toRootHex(signingRoot),
693
+ });
694
+
695
+ const signableMessage: SignableMessage = {
696
+ type: SignableMessageType.PAYLOAD_ATTESTATION,
697
+ data,
698
+ };
699
+
700
+ return {
701
+ validatorIndex: duty.validatorIndex,
702
+ data,
703
+ signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage),
704
+ };
705
+ }
706
+
671
707
  async signAttestationSelectionProof(pubkey: BLSPubkeyMaybeHex, slot: Slot): Promise<BLSSignature> {
672
708
  const signingSlot = slot;
673
709
  const domain = this.config.getDomain(slot, DOMAIN_SELECTION_PROOF);
@@ -852,6 +888,12 @@ export class ValidatorStore {
852
888
  }
853
889
  }
854
890
 
891
+ private validatePtcDuty(duty: routes.validator.PtcDuty, data: gloas.PayloadAttestationData): void {
892
+ if (duty.slot !== data.slot) {
893
+ throw Error(`Inconsistent PTC duties during signing: duty.slot ${duty.slot} != data.slot ${data.slot}`);
894
+ }
895
+ }
896
+
855
897
  private assertDoppelgangerSafe(pubKey: PubkeyHex | BLSPubkey): void {
856
898
  const pubkeyHex = typeof pubKey === "string" ? pubKey : toPubkeyHex(pubKey);
857
899
  if (!this.isDoppelgangerSafe(pubkeyHex)) {
@@ -34,6 +34,7 @@ export enum SignableMessageType {
34
34
  SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF = "SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF",
35
35
  VALIDATOR_REGISTRATION = "VALIDATOR_REGISTRATION",
36
36
  EXECUTION_PAYLOAD_ENVELOPE = "EXECUTION_PAYLOAD_ENVELOPE",
37
+ PAYLOAD_ATTESTATION = "PAYLOAD_ATTESTATION",
37
38
  }
38
39
 
39
40
  const AggregationSlotType = new ContainerType({
@@ -83,7 +84,8 @@ export type SignableMessage =
83
84
  | {type: SignableMessageType.SYNC_COMMITTEE_SELECTION_PROOF; data: ValueOf<typeof SyncAggregatorSelectionDataType>}
84
85
  | {type: SignableMessageType.SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF; data: altair.ContributionAndProof}
85
86
  | {type: SignableMessageType.VALIDATOR_REGISTRATION; data: ValidatorRegistrationV1}
86
- | {type: SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE; data: gloas.ExecutionPayloadEnvelope};
87
+ | {type: SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE; data: gloas.ExecutionPayloadEnvelope}
88
+ | {type: SignableMessageType.PAYLOAD_ATTESTATION; data: gloas.PayloadAttestationData};
87
89
 
88
90
  const requiresForkInfo: Record<SignableMessageType, boolean> = {
89
91
  [SignableMessageType.AGGREGATION_SLOT]: true,
@@ -99,6 +101,7 @@ const requiresForkInfo: Record<SignableMessageType, boolean> = {
99
101
  [SignableMessageType.SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF]: true,
100
102
  [SignableMessageType.VALIDATOR_REGISTRATION]: false,
101
103
  [SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE]: true,
104
+ [SignableMessageType.PAYLOAD_ATTESTATION]: true,
102
105
  };
103
106
 
104
107
  type Web3SignerSerializedRequest = {
@@ -273,6 +276,9 @@ function serializerSignableMessagePayload(config: BeaconConfig, payload: Signabl
273
276
 
274
277
  case SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE:
275
278
  return {execution_payload_envelope: ssz.gloas.ExecutionPayloadEnvelope.toJson(payload.data)};
279
+
280
+ case SignableMessageType.PAYLOAD_ATTESTATION:
281
+ return {payload_attestation: ssz.gloas.PayloadAttestationData.toJson(payload.data)};
276
282
  }
277
283
  }
278
284
 
package/src/validator.ts CHANGED
@@ -15,6 +15,7 @@ import {ValidatorEventEmitter} from "./services/emitter.js";
15
15
  import {ExternalSignerOptions, pollExternalSignerPubkeys} from "./services/externalSignerSync.js";
16
16
  import {IndicesService} from "./services/indices.js";
17
17
  import {pollBuilderValidatorRegistration, pollPrepareBeaconProposer} from "./services/prepareBeaconProposer.js";
18
+ import {PtcService} from "./services/ptc.js";
18
19
  import {SyncCommitteeService} from "./services/syncCommittee.js";
19
20
  import {SyncingStatusTracker} from "./services/syncingStatusTracker.js";
20
21
  import {Signer, ValidatorProposerConfig, ValidatorStore, defaultOptions} from "./services/validatorStore.js";
@@ -30,6 +31,7 @@ export type ValidatorModules = {
30
31
  slashingProtection: ISlashingProtection;
31
32
  blockProposingService: BlockProposingService;
32
33
  attestationService: AttestationService;
34
+ ptcService: PtcService;
33
35
  syncCommitteeService: SyncCommitteeService;
34
36
  config: BeaconConfig;
35
37
  api: ApiClient;
@@ -84,6 +86,7 @@ export class Validator {
84
86
  private readonly slashingProtection: ISlashingProtection;
85
87
  private readonly blockProposingService: BlockProposingService;
86
88
  private readonly attestationService: AttestationService;
89
+ private readonly ptcService: PtcService;
87
90
  private readonly syncCommitteeService: SyncCommitteeService;
88
91
  private readonly config: BeaconConfig;
89
92
  private readonly api: ApiClient;
@@ -102,6 +105,7 @@ export class Validator {
102
105
  slashingProtection,
103
106
  blockProposingService,
104
107
  attestationService,
108
+ ptcService,
105
109
  syncCommitteeService,
106
110
  config,
107
111
  api,
@@ -118,6 +122,7 @@ export class Validator {
118
122
  this.slashingProtection = slashingProtection;
119
123
  this.blockProposingService = blockProposingService;
120
124
  this.attestationService = attestationService;
125
+ this.ptcService = ptcService;
121
126
  this.syncCommitteeService = syncCommitteeService;
122
127
  this.config = config;
123
128
  this.api = api;
@@ -225,7 +230,7 @@ export class Validator {
225
230
  // We set infinity to prevent MaxListenersExceededWarning which get logged when listeners > 10
226
231
  emitter.setMaxListeners(Infinity);
227
232
 
228
- const chainHeaderTracker = new ChainHeaderTracker(logger, api, emitter);
233
+ const chainHeaderTracker = new ChainHeaderTracker(config, logger, api, emitter);
229
234
  const syncingStatusTracker = new SyncingStatusTracker(logger, api, clock, metrics);
230
235
 
231
236
  const blockProposingService = new BlockProposingService(config, loggerVc, api, clock, validatorStore, metrics, {
@@ -249,6 +254,18 @@ export class Validator {
249
254
  }
250
255
  );
251
256
 
257
+ const ptcService = new PtcService(
258
+ config,
259
+ loggerVc,
260
+ api,
261
+ clock,
262
+ validatorStore,
263
+ emitter,
264
+ chainHeaderTracker,
265
+ syncingStatusTracker,
266
+ metrics
267
+ );
268
+
252
269
  const syncCommitteeService = new SyncCommitteeService(
253
270
  config,
254
271
  loggerVc,
@@ -272,6 +289,7 @@ export class Validator {
272
289
  slashingProtection,
273
290
  blockProposingService,
274
291
  attestationService,
292
+ ptcService,
275
293
  syncCommitteeService,
276
294
  config,
277
295
  api,
@@ -338,6 +356,7 @@ export class Validator {
338
356
  removeDutiesForKey(pubkey: PubkeyHex): void {
339
357
  this.blockProposingService.removeDutiesForKey(pubkey);
340
358
  this.attestationService.removeDutiesForKey(pubkey);
359
+ this.ptcService.removeDutiesForKey(pubkey);
341
360
  this.syncCommitteeService.removeDutiesForKey(pubkey);
342
361
  }
343
362