@lodestar/beacon-node 1.42.0-dev.4411584fd8 → 1.42.0-dev.5f9f015475

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 (130) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +35 -16
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/chain/blocks/blockInput/types.d.ts +3 -3
  5. package/lib/chain/blocks/blockInput/types.d.ts.map +1 -1
  6. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  7. package/lib/chain/blocks/importBlock.js +18 -2
  8. package/lib/chain/blocks/importBlock.js.map +1 -1
  9. package/lib/chain/blocks/importExecutionPayload.d.ts +48 -0
  10. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -0
  11. package/lib/chain/blocks/importExecutionPayload.js +159 -0
  12. package/lib/chain/blocks/importExecutionPayload.js.map +1 -0
  13. package/lib/chain/blocks/payloadEnvelopeInput/index.d.ts +3 -0
  14. package/lib/chain/blocks/payloadEnvelopeInput/index.d.ts.map +1 -0
  15. package/lib/chain/blocks/payloadEnvelopeInput/index.js +3 -0
  16. package/lib/chain/blocks/payloadEnvelopeInput/index.js.map +1 -0
  17. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +80 -0
  18. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -0
  19. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +248 -0
  20. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -0
  21. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +29 -0
  22. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -0
  23. package/lib/chain/blocks/payloadEnvelopeInput/types.js +11 -0
  24. package/lib/chain/blocks/payloadEnvelopeInput/types.js.map +1 -0
  25. package/lib/chain/blocks/payloadEnvelopeProcessor.d.ts +15 -0
  26. package/lib/chain/blocks/payloadEnvelopeProcessor.d.ts.map +1 -0
  27. package/lib/chain/blocks/payloadEnvelopeProcessor.js +46 -0
  28. package/lib/chain/blocks/payloadEnvelopeProcessor.js.map +1 -0
  29. package/lib/chain/blocks/types.d.ts +7 -0
  30. package/lib/chain/blocks/types.d.ts.map +1 -1
  31. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.d.ts +12 -0
  32. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.d.ts.map +1 -0
  33. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.js +40 -0
  34. package/lib/chain/blocks/writePayloadEnvelopeInputToDb.js.map +1 -0
  35. package/lib/chain/chain.d.ts +7 -2
  36. package/lib/chain/chain.d.ts.map +1 -1
  37. package/lib/chain/chain.js +28 -3
  38. package/lib/chain/chain.js.map +1 -1
  39. package/lib/chain/errors/executionPayloadEnvelope.d.ts +12 -2
  40. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  41. package/lib/chain/errors/executionPayloadEnvelope.js +3 -1
  42. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  43. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  44. package/lib/chain/forkChoice/index.js +0 -10
  45. package/lib/chain/forkChoice/index.js.map +1 -1
  46. package/lib/chain/interface.d.ts +6 -3
  47. package/lib/chain/interface.d.ts.map +1 -1
  48. package/lib/chain/produceBlock/computeNewStateRoot.d.ts.map +1 -1
  49. package/lib/chain/produceBlock/computeNewStateRoot.js +6 -1
  50. package/lib/chain/produceBlock/computeNewStateRoot.js.map +1 -1
  51. package/lib/chain/regen/interface.d.ts +2 -0
  52. package/lib/chain/regen/interface.d.ts.map +1 -1
  53. package/lib/chain/regen/interface.js +2 -0
  54. package/lib/chain/regen/interface.js.map +1 -1
  55. package/lib/chain/seenCache/index.d.ts +1 -1
  56. package/lib/chain/seenCache/index.d.ts.map +1 -1
  57. package/lib/chain/seenCache/index.js +1 -1
  58. package/lib/chain/seenCache/index.js.map +1 -1
  59. package/lib/chain/seenCache/seenGossipBlockInput.js +2 -2
  60. package/lib/chain/seenCache/seenGossipBlockInput.js.map +1 -1
  61. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +38 -0
  62. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -0
  63. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +76 -0
  64. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -0
  65. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  66. package/lib/chain/validation/executionPayloadEnvelope.js +30 -19
  67. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  68. package/lib/chain/validation/syncCommittee.d.ts +2 -2
  69. package/lib/chain/validation/syncCommittee.d.ts.map +1 -1
  70. package/lib/chain/validation/syncCommittee.js +12 -11
  71. package/lib/chain/validation/syncCommittee.js.map +1 -1
  72. package/lib/chain/validatorMonitor.d.ts +2 -1
  73. package/lib/chain/validatorMonitor.d.ts.map +1 -1
  74. package/lib/chain/validatorMonitor.js +3 -0
  75. package/lib/chain/validatorMonitor.js.map +1 -1
  76. package/lib/metrics/metrics/lodestar.d.ts +28 -0
  77. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  78. package/lib/metrics/metrics/lodestar.js +74 -0
  79. package/lib/metrics/metrics/lodestar.js.map +1 -1
  80. package/lib/network/gossip/encoding.d.ts.map +1 -1
  81. package/lib/network/gossip/encoding.js +15 -0
  82. package/lib/network/gossip/encoding.js.map +1 -1
  83. package/lib/network/processor/extractSlotRootFns.d.ts.map +1 -1
  84. package/lib/network/processor/extractSlotRootFns.js +14 -4
  85. package/lib/network/processor/extractSlotRootFns.js.map +1 -1
  86. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  87. package/lib/network/processor/gossipHandlers.js +38 -8
  88. package/lib/network/processor/gossipHandlers.js.map +1 -1
  89. package/lib/sync/unknownBlock.d.ts +2 -8
  90. package/lib/sync/unknownBlock.d.ts.map +1 -1
  91. package/lib/sync/unknownBlock.js +7 -40
  92. package/lib/sync/unknownBlock.js.map +1 -1
  93. package/lib/util/sszBytes.d.ts +4 -1
  94. package/lib/util/sszBytes.d.ts.map +1 -1
  95. package/lib/util/sszBytes.js +69 -12
  96. package/lib/util/sszBytes.js.map +1 -1
  97. package/package.json +15 -15
  98. package/src/api/impl/beacon/blocks/index.ts +36 -17
  99. package/src/chain/blocks/blockInput/types.ts +3 -3
  100. package/src/chain/blocks/importBlock.ts +36 -2
  101. package/src/chain/blocks/importExecutionPayload.ts +241 -0
  102. package/src/chain/blocks/payloadEnvelopeInput/index.ts +2 -0
  103. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +336 -0
  104. package/src/chain/blocks/payloadEnvelopeInput/types.ts +33 -0
  105. package/src/chain/blocks/payloadEnvelopeProcessor.ts +61 -0
  106. package/src/chain/blocks/types.ts +8 -0
  107. package/src/chain/blocks/writePayloadEnvelopeInputToDb.ts +55 -0
  108. package/src/chain/chain.ts +37 -3
  109. package/src/chain/errors/executionPayloadEnvelope.ts +6 -2
  110. package/src/chain/forkChoice/index.ts +0 -10
  111. package/src/chain/interface.ts +6 -3
  112. package/src/chain/produceBlock/computeNewStateRoot.ts +6 -1
  113. package/src/chain/regen/interface.ts +2 -0
  114. package/src/chain/seenCache/index.ts +1 -1
  115. package/src/chain/seenCache/seenGossipBlockInput.ts +2 -2
  116. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +106 -0
  117. package/src/chain/validation/executionPayloadEnvelope.ts +38 -25
  118. package/src/chain/validation/syncCommittee.ts +15 -14
  119. package/src/chain/validatorMonitor.ts +10 -0
  120. package/src/metrics/metrics/lodestar.ts +77 -0
  121. package/src/network/gossip/encoding.ts +16 -0
  122. package/src/network/processor/extractSlotRootFns.ts +18 -5
  123. package/src/network/processor/gossipHandlers.ts +44 -7
  124. package/src/sync/unknownBlock.ts +9 -49
  125. package/src/util/sszBytes.ts +90 -10
  126. package/lib/chain/seenCache/seenExecutionPayloadEnvelope.d.ts +0 -15
  127. package/lib/chain/seenCache/seenExecutionPayloadEnvelope.d.ts.map +0 -1
  128. package/lib/chain/seenCache/seenExecutionPayloadEnvelope.js +0 -28
  129. package/lib/chain/seenCache/seenExecutionPayloadEnvelope.js.map +0 -1
  130. package/src/chain/seenCache/seenExecutionPayloadEnvelope.ts +0 -34
@@ -15,12 +15,12 @@ export async function validateGossipSyncCommittee(
15
15
  chain: IBeaconChain,
16
16
  syncCommittee: altair.SyncCommitteeMessage,
17
17
  subnet: SubnetID
18
- ): Promise<{indexInSubcommittee: IndexInSubcommittee}> {
18
+ ): Promise<{indicesInSubcommittee: IndexInSubcommittee[]}> {
19
19
  const {slot, validatorIndex, beaconBlockRoot} = syncCommittee;
20
20
  const messageRoot = toRootHex(beaconBlockRoot);
21
21
 
22
22
  const headState = chain.getHeadState();
23
- const indexInSubcommittee = validateGossipSyncCommitteeExceptSig(chain, headState, subnet, syncCommittee);
23
+ const indicesInSubcommittee = validateGossipSyncCommitteeExceptSig(chain, headState, subnet, syncCommittee);
24
24
 
25
25
  // [IGNORE] The signature's slot is for the current slot, i.e. sync_committee_signature.slot == current_slot.
26
26
  // > Checked in validateGossipSyncCommitteeExceptSig()
@@ -68,7 +68,7 @@ export async function validateGossipSyncCommittee(
68
68
  // Register this valid item as seen
69
69
  chain.seenSyncCommitteeMessages.add(slot, subnet, validatorIndex, messageRoot);
70
70
 
71
- return {indexInSubcommittee};
71
+ return {indicesInSubcommittee};
72
72
  }
73
73
 
74
74
  export async function validateApiSyncCommittee(
@@ -105,7 +105,7 @@ export function validateGossipSyncCommitteeExceptSig(
105
105
  headState: CachedBeaconStateAllForks,
106
106
  subnet: SubnetID,
107
107
  data: Pick<altair.SyncCommitteeMessage, "slot" | "validatorIndex">
108
- ): IndexInSubcommittee {
108
+ ): IndexInSubcommittee[] {
109
109
  const {slot, validatorIndex} = data;
110
110
  // [IGNORE] The signature's slot is for the current slot, i.e. sync_committee_signature.slot == current_slot.
111
111
  // (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance)
@@ -127,26 +127,27 @@ export function validateGossipSyncCommitteeExceptSig(
127
127
 
128
128
  // [REJECT] The subnet_id is valid for the given validator, i.e. subnet_id in compute_subnets_for_sync_committee(state, sync_committee_signature.validator_index).
129
129
  // Note this validation implies the validator is part of the broader current sync committee along with the correct subcommittee.
130
- const indexInSubcommittee = getIndexInSubcommittee(headState, subnet, data);
131
- if (indexInSubcommittee === null) {
130
+ const indicesInSubcommittee = getIndicesInSubcommittee(headState, subnet, data);
131
+ if (indicesInSubcommittee === null) {
132
132
  throw new SyncCommitteeError(GossipAction.REJECT, {
133
133
  code: SyncCommitteeErrorCode.VALIDATOR_NOT_IN_SYNC_COMMITTEE,
134
134
  validatorIndex,
135
135
  });
136
136
  }
137
137
 
138
- return indexInSubcommittee;
138
+ return indicesInSubcommittee;
139
139
  }
140
140
 
141
141
  /**
142
- * Returns the IndexInSubcommittee of the given `subnet`.
143
- * Returns `null` if not part of the sync committee or not part of the given `subnet`
142
+ * Returns all IndexInSubcommittee positions of the given `subnet`.
143
+ * Returns `null` if not part of the sync committee or not part of the given `subnet`.
144
+ * A validator may appear multiple times in the same subcommittee.
144
145
  */
145
- function getIndexInSubcommittee(
146
+ function getIndicesInSubcommittee(
146
147
  headState: CachedBeaconStateAllForks,
147
148
  subnet: SubnetID,
148
149
  data: Pick<altair.SyncCommitteeMessage, "slot" | "validatorIndex">
149
- ): IndexInSubcommittee | null {
150
+ ): IndexInSubcommittee[] | null {
150
151
  const syncCommittee = headState.epochCtx.getIndexedSyncCommittee(data.slot);
151
152
  const indexesInCommittee = syncCommittee.validatorIndexMap.get(data.validatorIndex);
152
153
  if (indexesInCommittee === undefined) {
@@ -154,12 +155,12 @@ function getIndexInSubcommittee(
154
155
  return null;
155
156
  }
156
157
 
158
+ const indices: IndexInSubcommittee[] = [];
157
159
  for (const indexInCommittee of indexesInCommittee) {
158
160
  if (Math.floor(indexInCommittee / SYNC_COMMITTEE_SUBNET_SIZE) === subnet) {
159
- return indexInCommittee % SYNC_COMMITTEE_SUBNET_SIZE;
161
+ indices.push(indexInCommittee % SYNC_COMMITTEE_SUBNET_SIZE);
160
162
  }
161
163
  }
162
164
 
163
- // Not part of this specific subnet
164
- return null;
165
+ return indices.length > 0 ? indices : null;
165
166
  }
@@ -23,6 +23,7 @@ import {
23
23
  ValidatorIndex,
24
24
  altair,
25
25
  deneb,
26
+ gloas,
26
27
  } from "@lodestar/types";
27
28
  import {LogData, LogHandler, LogLevel, Logger, MapDef, MapDefMax, prettyPrintIndices, toRootHex} from "@lodestar/utils";
28
29
  import {GENESIS_SLOT} from "../constants/constants.js";
@@ -61,6 +62,11 @@ export type ValidatorMonitor = {
61
62
  ): void;
62
63
  registerBeaconBlock(src: OpSource, delaySec: Seconds, block: BeaconBlock): void;
63
64
  registerBlobSidecar(src: OpSource, seenTimestampSec: Seconds, blob: deneb.BlobSidecar): void;
65
+ registerExecutionPayloadEnvelope(
66
+ src: OpSource,
67
+ delaySec: Seconds,
68
+ envelope: gloas.SignedExecutionPayloadEnvelope
69
+ ): void;
64
70
  registerImportedBlock(block: BeaconBlock, data: {proposerBalanceDelta: number}): void;
65
71
  onPoolSubmitUnaggregatedAttestation(
66
72
  seenTimestampSec: number,
@@ -450,6 +456,10 @@ export function createValidatorMonitor(
450
456
  //TODO: freetheblobs
451
457
  },
452
458
 
459
+ registerExecutionPayloadEnvelope(_src, _delaySec, _envelope) {
460
+ // TODO GLOAS: implement execution payload envelope monitoring
461
+ },
462
+
453
463
  registerImportedBlock(block, {proposerBalanceDelta}) {
454
464
  const validator = validators.get(block.proposerIndex);
455
465
  if (validator) {
@@ -3,6 +3,7 @@ import {NotReorgedReason} from "@lodestar/fork-choice";
3
3
  import {ArchiveStoreTask} from "../../chain/archiveStore/archiveStore.js";
4
4
  import {FrequencyStateArchiveStep} from "../../chain/archiveStore/strategies/frequencyStateArchiveStrategy.js";
5
5
  import {BlockInputSource} from "../../chain/blocks/blockInput/index.js";
6
+ import {PayloadEnvelopeInputSource} from "../../chain/blocks/payloadEnvelopeInput/index.js";
6
7
  import {JobQueueItemType} from "../../chain/bls/index.js";
7
8
  import {AttestationErrorCode, BlockErrorCode} from "../../chain/errors/index.js";
8
9
  import {
@@ -237,6 +238,56 @@ export function createLodestarMetrics(
237
238
  }),
238
239
  },
239
240
 
241
+ payloadEnvelopeProcessorQueue: {
242
+ length: register.gauge({
243
+ name: "lodestar_payload_envelope_processor_queue_length",
244
+ help: "Count of total payload envelope processor queue length",
245
+ }),
246
+ droppedJobs: register.gauge({
247
+ name: "lodestar_payload_envelope_processor_queue_dropped_jobs_total",
248
+ help: "Count of total payload envelope processor queue dropped jobs",
249
+ }),
250
+ jobTime: register.histogram({
251
+ name: "lodestar_payload_envelope_processor_queue_job_time_seconds",
252
+ help: "Time to process payload envelope processor queue job in seconds",
253
+ buckets: [0.01, 0.1, 1, 4, 12],
254
+ }),
255
+ jobWaitTime: register.histogram({
256
+ name: "lodestar_payload_envelope_processor_queue_job_wait_time_seconds",
257
+ help: "Time from job added to the payload envelope processor queue to starting in seconds",
258
+ buckets: [0.01, 0.1, 1, 4, 12],
259
+ }),
260
+ concurrency: register.gauge({
261
+ name: "lodestar_payload_envelope_processor_queue_concurrency",
262
+ help: "Current concurrency of payload envelope processor queue",
263
+ }),
264
+ },
265
+
266
+ unfinalizedPayloadEnvelopeWritesQueue: {
267
+ length: register.gauge({
268
+ name: "lodestar_unfinalized_payload_envelope_writes_queue_length",
269
+ help: "Count of total unfinalized payload envelope writes queue length",
270
+ }),
271
+ droppedJobs: register.gauge({
272
+ name: "lodestar_unfinalized_payload_envelope_writes_queue_dropped_jobs_total",
273
+ help: "Count of total unfinalized payload envelope writes queue dropped jobs",
274
+ }),
275
+ jobTime: register.histogram({
276
+ name: "lodestar_unfinalized_payload_envelope_writes_queue_job_time_seconds",
277
+ help: "Time to process unfinalized payload envelope writes queue job in seconds",
278
+ buckets: [0.01, 0.1, 1, 4, 12],
279
+ }),
280
+ jobWaitTime: register.histogram({
281
+ name: "lodestar_unfinalized_payload_envelope_writes_queue_job_wait_time_seconds",
282
+ help: "Time from job added to the unfinalized payload envelope writes queue to starting in seconds",
283
+ buckets: [0.01, 0.1, 1, 4, 12],
284
+ }),
285
+ concurrency: register.gauge({
286
+ name: "lodestar_unfinalized_payload_envelope_writes_queue_concurrency",
287
+ help: "Current concurrency of unfinalized payload envelope writes queue",
288
+ }),
289
+ },
290
+
240
291
  engineHttpProcessorQueue: {
241
292
  length: register.gauge({
242
293
  name: "lodestar_engine_http_processor_queue_length",
@@ -925,6 +976,18 @@ export function createLodestarMetrics(
925
976
  labelNames: ["reason"],
926
977
  }),
927
978
  },
979
+ importPayload: {
980
+ bySource: register.gauge<{source: PayloadEnvelopeInputSource}>({
981
+ name: "lodestar_import_payload_by_source_total",
982
+ help: "Total number of imported execution payload envelopes by source",
983
+ labelNames: ["source"],
984
+ }),
985
+ columnsBySource: register.gauge<{source: PayloadEnvelopeInputSource}>({
986
+ name: "lodestar_import_payload_columns_by_source_total",
987
+ help: "Total number of payload-attached columns (sampled columns for Gloas) by source",
988
+ labelNames: ["source"],
989
+ }),
990
+ },
928
991
  engineNotifyNewPayloadResult: register.gauge<{result: ExecutionPayloadStatus}>({
929
992
  name: "lodestar_execution_engine_notify_new_payload_result_total",
930
993
  help: "The total result of calling notifyNewPayload execution engine api",
@@ -1495,6 +1558,20 @@ export function createLodestarMetrics(
1495
1558
  help: "Number of BlockInputs created via a data column being seen first",
1496
1559
  }),
1497
1560
  },
1561
+ payloadEnvelopeInput: {
1562
+ count: register.gauge({
1563
+ name: "lodestar_seen_payload_envelope_input_cache_size",
1564
+ help: "Number of cached PayloadEnvelopeInputs",
1565
+ }),
1566
+ serializedObjectRefs: register.gauge({
1567
+ name: "lodestar_seen_payload_envelope_input_cache_serialized_object_refs",
1568
+ help: "Number of serialized-cache object refs retained by cached PayloadEnvelopeInputs",
1569
+ }),
1570
+ created: register.counter({
1571
+ name: "lodestar_seen_payload_envelope_input_cache_items_created_total",
1572
+ help: "Number of PayloadEnvelopeInputs created",
1573
+ }),
1574
+ },
1498
1575
  },
1499
1576
 
1500
1577
  processFinalizedCheckpoint: {
@@ -24,12 +24,28 @@ const decoder = new snappyWasm.Decoder();
24
24
  // Shared buffer to convert msgId to string
25
25
  const sharedMsgIdBuf = Buffer.alloc(20);
26
26
 
27
+ // Cache topic -> seed to avoid per-message allocations on the hot path.
28
+ // Topics are a fixed set per fork (changes only at fork boundaries).
29
+ const topicSeedCache = new Map<string, bigint>();
30
+
27
31
  /**
28
32
  * The function used to generate a gossipsub message id
29
33
  * We use the first 8 bytes of SHA256(data) for content addressing
30
34
  */
31
35
  export function fastMsgIdFn(rpcMsg: RPC.Message): string {
32
36
  if (rpcMsg.data) {
37
+ if (rpcMsg.topic) {
38
+ // Use topic-derived seed to prevent cross-topic deduplication of identical messages.
39
+ // SyncCommitteeMessages are published to multiple sync_committee_{subnet} topics with
40
+ // identical data, so hashing only the data incorrectly deduplicates across subnets.
41
+ // See https://github.com/ChainSafe/lodestar/issues/8294
42
+ let topicSeed = topicSeedCache.get(rpcMsg.topic);
43
+ if (topicSeed === undefined) {
44
+ topicSeed = xxhash.h64Raw(Buffer.from(rpcMsg.topic), h64Seed);
45
+ topicSeedCache.set(rpcMsg.topic, topicSeed);
46
+ }
47
+ return xxhash.h64Raw(rpcMsg.data, topicSeed).toString(16);
48
+ }
33
49
  return xxhash.h64Raw(rpcMsg.data, h64Seed).toString(16);
34
50
  }
35
51
  return "0000000000000000";
@@ -1,11 +1,14 @@
1
- import {ForkName} from "@lodestar/params";
1
+ import {ForkName, isForkPostGloas} from "@lodestar/params";
2
2
  import {SlotOptionalRoot, SlotRootHex} from "@lodestar/types";
3
3
  import {
4
+ getBeaconBlockRootFromDataColumnSidecarSerialized,
5
+ getBeaconBlockRootFromExecutionPayloadEnvelopeSerialized,
4
6
  getBlockRootFromBeaconAttestationSerialized,
5
7
  getBlockRootFromSignedAggregateAndProofSerialized,
6
8
  getSlotFromBeaconAttestationSerialized,
7
9
  getSlotFromBlobSidecarSerialized,
8
10
  getSlotFromDataColumnSidecarSerialized,
11
+ getSlotFromExecutionPayloadEnvelopeSerialized,
9
12
  getSlotFromSignedAggregateAndProofSerialized,
10
13
  getSlotFromSignedBeaconBlockSerialized,
11
14
  } from "../../util/sszBytes.js";
@@ -52,13 +55,23 @@ export function createExtractBlockSlotRootFns(): ExtractSlotRootFns {
52
55
  }
53
56
  return {slot};
54
57
  },
55
- [GossipType.data_column_sidecar]: (data: Uint8Array): SlotOptionalRoot | null => {
56
- const slot = getSlotFromDataColumnSidecarSerialized(data);
57
-
58
+ [GossipType.data_column_sidecar]: (data: Uint8Array, fork: ForkName): SlotOptionalRoot | null => {
59
+ const slot = getSlotFromDataColumnSidecarSerialized(data, fork);
58
60
  if (slot === null) {
59
61
  return null;
60
62
  }
61
- return {slot};
63
+
64
+ const root = isForkPostGloas(fork) ? getBeaconBlockRootFromDataColumnSidecarSerialized(data) : null;
65
+ return root !== null ? {slot, root} : {slot};
66
+ },
67
+ [GossipType.execution_payload]: (data: Uint8Array): SlotRootHex | null => {
68
+ const slot = getSlotFromExecutionPayloadEnvelopeSerialized(data);
69
+ const root = getBeaconBlockRootFromExecutionPayloadEnvelopeSerialized(data);
70
+
71
+ if (slot === null || root === null) {
72
+ return null;
73
+ }
74
+ return {slot, root};
62
75
  },
63
76
  };
64
77
  }
@@ -30,6 +30,7 @@ import {
30
30
  IBlockInput,
31
31
  isBlockInputColumns,
32
32
  } from "../../chain/blocks/blockInput/index.js";
33
+ import {PayloadEnvelopeInputSource} from "../../chain/blocks/payloadEnvelopeInput/index.js";
33
34
  import {BlobSidecarValidation} from "../../chain/blocks/types.js";
34
35
  import {ChainEvent} from "../../chain/emitter.js";
35
36
  import {
@@ -42,6 +43,8 @@ import {
42
43
  BlockGossipError,
43
44
  DataColumnSidecarErrorCode,
44
45
  DataColumnSidecarGossipError,
46
+ ExecutionPayloadEnvelopeError,
47
+ ExecutionPayloadEnvelopeErrorCode,
45
48
  GossipAction,
46
49
  GossipActionError,
47
50
  SyncCommitteeError,
@@ -616,6 +619,13 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
616
619
  });
617
620
  });
618
621
  }
622
+
623
+ // TODO GLOAS: In Gloas, also add column to PayloadEnvelopeInput and notify the payload processor:
624
+ // const payloadInput = chain.seenPayloadEnvelopeInput.get(blockRootHex);
625
+ // if (payloadInput) {
626
+ // payloadInput.addColumn({columnSidecar, source: BlockInputSource.gossip, seenTimestampSec, peerIdStr});
627
+ // chain.processExecutionPayload(payloadInput, {validSignature: true});
628
+ // }
619
629
  },
620
630
 
621
631
  [GossipType.beacon_aggregate_and_proof]: async ({
@@ -767,9 +777,9 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
767
777
  const {serializedData} = gossipData;
768
778
  const syncCommittee = sszDeserialize(topic, serializedData);
769
779
  const {subnet} = topic;
770
- let indexInSubcommittee = 0;
780
+ let indicesInSubcommittee: number[] = [0];
771
781
  try {
772
- indexInSubcommittee = (await validateGossipSyncCommittee(chain, syncCommittee, subnet)).indexInSubcommittee;
782
+ indicesInSubcommittee = (await validateGossipSyncCommittee(chain, syncCommittee, subnet)).indicesInSubcommittee;
773
783
  } catch (e) {
774
784
  if (e instanceof SyncCommitteeError && e.action === GossipAction.REJECT) {
775
785
  chain.persistInvalidSszValue(ssz.altair.SyncCommitteeMessage, syncCommittee, "gossip_reject");
@@ -777,11 +787,12 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
777
787
  throw e;
778
788
  }
779
789
 
780
- // Handler
781
-
790
+ // Handler — add for ALL positions this validator holds in the subcommittee
782
791
  try {
783
- const insertOutcome = chain.syncCommitteeMessagePool.add(subnet, syncCommittee, indexInSubcommittee);
784
- metrics?.opPool.syncCommitteeMessagePoolInsertOutcome.inc({insertOutcome});
792
+ for (const indexInSubcommittee of indicesInSubcommittee) {
793
+ const insertOutcome = chain.syncCommitteeMessagePool.add(subnet, syncCommittee, indexInSubcommittee);
794
+ metrics?.opPool.syncCommitteeMessagePoolInsertOutcome.inc({insertOutcome});
795
+ }
785
796
  } catch (e) {
786
797
  logger.debug("Error adding to syncCommittee pool", {subnet}, e as Error);
787
798
  }
@@ -826,17 +837,43 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
826
837
  [GossipType.execution_payload]: async ({
827
838
  gossipData,
828
839
  topic,
840
+ peerIdStr,
829
841
  seenTimestampSec,
830
842
  }: GossipHandlerParamGeneric<GossipType.execution_payload>) => {
831
843
  const {serializedData} = gossipData;
832
844
  const executionPayloadEnvelope = sszDeserialize(topic, serializedData);
845
+ // TODO GLOAS: handle BLOCK_ROOT_UNKNOWN error to trigger sync
833
846
  await validateGossipExecutionPayloadEnvelope(chain, executionPayloadEnvelope);
834
847
 
835
848
  const slot = executionPayloadEnvelope.message.slot;
836
849
  const delaySec = seenTimestampSec - computeTimeAtSlot(config, slot, chain.genesisTime);
837
850
  metrics?.gossipExecutionPayloadEnvelope.elapsedTimeTillReceived.observe({source: OpSource.gossip}, delaySec);
851
+ chain.validatorMonitor?.registerExecutionPayloadEnvelope(OpSource.gossip, delaySec, executionPayloadEnvelope);
852
+
853
+ const blockRootHex = toRootHex(executionPayloadEnvelope.message.beaconBlockRoot);
854
+ const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex);
855
+
856
+ if (!payloadInput) {
857
+ // This shouldn't happen because beacon block should have been imported and thus payload input should have been created.
858
+ throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, {
859
+ code: ExecutionPayloadEnvelopeErrorCode.PAYLOAD_ENVELOPE_INPUT_MISSING,
860
+ blockRoot: blockRootHex,
861
+ });
862
+ }
863
+
864
+ chain.serializedCache.set(executionPayloadEnvelope, serializedData);
865
+
866
+ payloadInput.addPayloadEnvelope({
867
+ envelope: executionPayloadEnvelope,
868
+ source: PayloadEnvelopeInputSource.gossip,
869
+ seenTimestampSec,
870
+ peerIdStr,
871
+ });
838
872
 
839
- // TODO GLOAS: Handle valid envelope. Need an import flow that calls `processExecutionPayloadEnvelope` and fork choice
873
+ // TODO GLOAS: Emit execution_payload_gossip event for gossip receipt.
874
+ chain.processExecutionPayload(payloadInput, {validSignature: true}).catch((e) => {
875
+ chain.logger.debug("Error processing execution payload from gossip", {slot, root: blockRootHex}, e as Error);
876
+ });
840
877
  },
841
878
  [GossipType.payload_attestation_message]: async ({
842
879
  gossipData,
@@ -481,7 +481,7 @@ export class BlockInputSync {
481
481
  * From a set of shuffled peers:
482
482
  * - fetch the block
483
483
  * - from deneb, fetch all missing blobs
484
- * - from peerDAS, fetch sampled colmns
484
+ * - from peerDAS, fetch sampled columns
485
485
  * TODO: this means we only have block root, and nothing else. Consider to reflect this in the function name
486
486
  * prefulu, will attempt a max of `MAX_ATTEMPTS_PER_BLOCK` on different peers, postfulu we may attempt more as defined in `getMaxDownloadAttempts()` function
487
487
  * Also verifies the received block root + returns the peer that provided the block for future downscoring.
@@ -489,10 +489,7 @@ export class BlockInputSync {
489
489
  private async fetchBlockInput(cacheItem: BlockInputSyncCacheItem): Promise<PendingBlockInput> {
490
490
  const rootHex = getBlockInputSyncCacheItemRootHex(cacheItem);
491
491
  const excludedPeers = new Set<PeerIdStr>();
492
- const defaultPendingColumns =
493
- this.config.getForkSeq(this.chain.clock.currentSlot) >= ForkSeq.fulu
494
- ? new Set(this.network.custodyConfig.sampledColumns)
495
- : null;
492
+ const defaultPendingColumns = new Set(this.network.custodyConfig.sampledColumns);
496
493
 
497
494
  const fetchStartSec = Date.now() / 1000;
498
495
  let slot = isPendingBlockInput(cacheItem) ? cacheItem.blockInput.slot : undefined;
@@ -506,14 +503,10 @@ export class BlockInputSync {
506
503
  isPendingBlockInput(cacheItem) && isBlockInputColumns(cacheItem.blockInput)
507
504
  ? new Set(cacheItem.blockInput.getMissingSampledColumnMeta().missing)
508
505
  : defaultPendingColumns;
509
- // pendingDataColumns is null pre-fulu
510
506
  const peerMeta = this.peerBalancer.bestPeerForPendingColumns(pendingColumns, excludedPeers);
511
507
  if (peerMeta === null) {
512
508
  // no more peer with needed columns to try, throw error
513
- let message = `Error fetching UnknownBlockRoot slot=${slot} root=${rootHex} after ${i}: cannot find peer`;
514
- if (pendingColumns) {
515
- message += ` with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`;
516
- }
509
+ const message = `Error fetching UnknownBlockRoot slot=${slot} root=${rootHex} after ${i}: cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`;
517
510
  this.metrics?.blockInputSync.fetchTimeSec.observe(
518
511
  {result: FetchResult.FailureTriedAllPeers},
519
512
  Date.now() / 1000 - fetchStartSec
@@ -650,7 +643,7 @@ export class BlockInputSync {
650
643
  // TODO(fulu): why is this commented out here?
651
644
  //
652
645
  // this.knownBadBlocks.add(block.blockRootHex);
653
- // for (const peerIdStr of block.peerIdStrs) {
646
+ // for (const peerIdStr of block.peerIdStrings) {
654
647
  // // TODO: Refactor peerRpcScores to work with peerIdStr only
655
648
  // this.network.reportPeer(peerIdStr, PeerAction.LowToleranceError, "BadBlockByRoot");
656
649
  // }
@@ -729,11 +722,11 @@ export class UnknownBlockPeerBalancer {
729
722
  }
730
723
 
731
724
  /**
732
- * called from fetchUnknownBlockRoot() where we only have block root and nothing else
725
+ * called from fetchBlockInput() where we only have block root and nothing else
733
726
  * excludedPeers are the peers that we requested already so we don't want to try again
734
727
  * pendingColumns is empty for prefulu, or the 1st time we we download a block by root
735
728
  */
736
- bestPeerForPendingColumns(pendingColumns: Set<number> | null, excludedPeers: Set<PeerIdStr>): PeerSyncMeta | null {
729
+ bestPeerForPendingColumns(pendingColumns: Set<number>, excludedPeers: Set<PeerIdStr>): PeerSyncMeta | null {
737
730
  const eligiblePeers = this.filterPeers(pendingColumns, excludedPeers);
738
731
  if (eligiblePeers.length === 0) {
739
732
  return null;
@@ -750,37 +743,6 @@ export class UnknownBlockPeerBalancer {
750
743
  return this.peersMeta.get(bestPeerId) ?? null;
751
744
  }
752
745
 
753
- /**
754
- * called from fetchUnavailableBlockInput() where we have either BlockInput or NullBlockInput
755
- * excludedPeers are the peers that we requested already so we don't want to try again
756
- */
757
- bestPeerForBlockInput(blockInput: IBlockInput, excludedPeers: Set<PeerIdStr>): PeerSyncMeta | null {
758
- const eligiblePeers: PeerIdStr[] = [];
759
-
760
- if (isBlockInputColumns(blockInput)) {
761
- const pendingDataColumns: Set<number> = new Set(blockInput.getMissingSampledColumnMeta().missing);
762
- // there could be no pending column in case when block is still missing
763
- eligiblePeers.push(...this.filterPeers(pendingDataColumns, excludedPeers));
764
- } else {
765
- // prefulu
766
- eligiblePeers.push(...this.filterPeers(null, excludedPeers));
767
- }
768
-
769
- if (eligiblePeers.length === 0) {
770
- return null;
771
- }
772
-
773
- const sortedEligiblePeers = sortBy(
774
- shuffle(eligiblePeers),
775
- // prefer peers with least active req
776
- (peerId) => this.activeRequests.get(peerId) ?? 0
777
- );
778
-
779
- const bestPeerId = sortedEligiblePeers[0];
780
- this.onRequest(bestPeerId);
781
- return this.peersMeta.get(bestPeerId) ?? null;
782
- }
783
-
784
746
  /**
785
747
  * Consumers don't need to call this method directly, it is called internally by bestPeer*() methods
786
748
  * make this public for testing
@@ -804,8 +766,7 @@ export class UnknownBlockPeerBalancer {
804
766
  return totalActiveRequests;
805
767
  }
806
768
 
807
- // pendingDataColumns could be null for prefulu
808
- private filterPeers(pendingDataColumns: Set<number> | null, excludedPeers: Set<PeerIdStr>): PeerIdStr[] {
769
+ private filterPeers(pendingDataColumns: Set<number>, excludedPeers: Set<PeerIdStr>): PeerIdStr[] {
809
770
  let maxColumnCount = 0;
810
771
  const considerPeers: {peerId: PeerIdStr; columnCount: number}[] = [];
811
772
  for (const [peerId, syncMeta] of this.peersMeta.entries()) {
@@ -820,13 +781,12 @@ export class UnknownBlockPeerBalancer {
820
781
  continue;
821
782
  }
822
783
 
823
- if (pendingDataColumns === null || pendingDataColumns.size === 0) {
824
- // prefulu, no pending columns
784
+ if (pendingDataColumns.size === 0) {
825
785
  considerPeers.push({peerId, columnCount: 0});
826
786
  continue;
827
787
  }
828
788
 
829
- // postfulu, find peers that have custody columns that we need
789
+ // find peers that have custody columns that we need
830
790
  const {custodyColumns: peerColumns} = syncMeta;
831
791
  // check if the peer has all needed columns
832
792
  // get match
@@ -8,6 +8,7 @@ import {
8
8
  ForkSeq,
9
9
  MAX_COMMITTEES_PER_SLOT,
10
10
  isForkPostElectra,
11
+ isForkPostGloas,
11
12
  } from "@lodestar/params";
12
13
  import {BLSSignature, CommitteeIndex, RootHex, Slot, ValidatorIndex, ssz} from "@lodestar/types";
13
14
 
@@ -398,23 +399,102 @@ export function getSlotFromBlobSidecarSerialized(data: Uint8Array): Slot | null
398
399
  }
399
400
 
400
401
  /**
402
+ * Pre-Gloas DataColumnSidecar:
401
403
  * {
402
- index: ColumnIndex [ fixed - 8 bytes],
403
- column: DataColumn BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_CELL * <some non fixed length>,
404
- kzgCommitments: denebSsz.BlobKzgCommitments,
405
- kzgProofs: denebSsz.KZGProofs,
406
- signedBlockHeader: phase0Ssz.SignedBeaconBlockHeader,
407
- kzgCommitmentsInclusionProof: KzgCommitmentsInclusionProof,
404
+ * index: ColumnIndex [fixed - 8 bytes],
405
+ * column: DataColumn (offset - 4 bytes),
406
+ * kzgCommitments: (offset - 4 bytes),
407
+ * kzgProofs: (offset - 4 bytes),
408
+ * signedBlockHeader: (offset - 4 bytes) -> slot at variable offset after fixed header
409
+ * kzgCommitmentsInclusionProof: (offset - 4 bytes),
410
+ * }
411
+ * Post-Gloas DataColumnSidecar:
412
+ * {
413
+ * index: ColumnIndex [8 bytes],
414
+ * column: DataColumn (offset - 4 bytes),
415
+ * kzgProofs: (offset - 4 bytes),
416
+ * slot: Slot [8 bytes] - at offset 16,
417
+ * beaconBlockRoot: Root [32 bytes] - at offset 24,
418
+ * }
419
+ */
420
+ const SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR_PRE_GLOAS = 20;
421
+ const SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR_POST_GLOAS = 16;
422
+ const BEACON_BLOCK_ROOT_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR = 24;
423
+
424
+ export function getSlotFromDataColumnSidecarSerialized(data: Uint8Array, fork: ForkName): Slot | null {
425
+ const offset = isForkPostGloas(fork)
426
+ ? SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR_POST_GLOAS
427
+ : SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR_PRE_GLOAS;
428
+
429
+ if (data.length < offset + SLOT_SIZE) {
430
+ return null;
408
431
  }
432
+
433
+ return getSlotFromOffset(data, offset);
434
+ }
435
+
436
+ export function getBeaconBlockRootFromDataColumnSidecarSerialized(data: Uint8Array): RootHex | null {
437
+ if (data.length < BEACON_BLOCK_ROOT_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR + ROOT_SIZE) {
438
+ return null;
439
+ }
440
+
441
+ blockRootBuf.set(
442
+ data.subarray(
443
+ BEACON_BLOCK_ROOT_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR,
444
+ BEACON_BLOCK_ROOT_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR + ROOT_SIZE
445
+ )
446
+ );
447
+ return "0x" + blockRootBuf.toString("hex");
448
+ }
449
+
450
+ /**
451
+ * SignedExecutionPayloadEnvelope SSZ Layout:
452
+ * ├─ 4 bytes: message offset (points to byte 100)
453
+ * ├─ 96 bytes: signature
454
+ * └─ ExecutionPayloadEnvelope (starts at byte 100):
455
+ * ├─ 4 bytes: payload offset
456
+ * ├─ 4 bytes: executionRequests offset
457
+ * ├─ 8 bytes: builderIndex (offset 108-115)
458
+ * ├─ 32 bytes: beaconBlockRoot (offset 116-147)
459
+ * ├─ 8 bytes: slot (offset 148-155)
460
+ * └─ 32 bytes: stateRoot (offset 156-187)
409
461
  */
462
+ const SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MESSAGE_OFFSET = 4;
463
+ const SIGNED_EXECUTION_PAYLOAD_ENVELOPE_SIGNATURE_SIZE = 96;
464
+ const EXECUTION_PAYLOAD_ENVELOPE_PAYLOAD_OFFSET = 4;
465
+ const EXECUTION_PAYLOAD_ENVELOPE_REQUESTS_OFFSET = 4;
466
+ const EXECUTION_PAYLOAD_ENVELOPE_BUILDER_INDEX_SIZE = 8;
467
+
468
+ const BEACON_BLOCK_ROOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE =
469
+ SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MESSAGE_OFFSET +
470
+ SIGNED_EXECUTION_PAYLOAD_ENVELOPE_SIGNATURE_SIZE +
471
+ EXECUTION_PAYLOAD_ENVELOPE_PAYLOAD_OFFSET +
472
+ EXECUTION_PAYLOAD_ENVELOPE_REQUESTS_OFFSET +
473
+ EXECUTION_PAYLOAD_ENVELOPE_BUILDER_INDEX_SIZE; // 116
474
+
475
+ const SLOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE =
476
+ BEACON_BLOCK_ROOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + ROOT_SIZE; // 148
477
+
478
+ export function getSlotFromExecutionPayloadEnvelopeSerialized(data: Uint8Array): Slot | null {
479
+ if (data.length < SLOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + SLOT_SIZE) {
480
+ return null;
481
+ }
410
482
 
411
- const SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR = 20;
412
- export function getSlotFromDataColumnSidecarSerialized(data: Uint8Array): Slot | null {
413
- if (data.length < SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR + SLOT_SIZE) {
483
+ return getSlotFromOffset(data, SLOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE);
484
+ }
485
+
486
+ export function getBeaconBlockRootFromExecutionPayloadEnvelopeSerialized(data: Uint8Array): RootHex | null {
487
+ if (data.length < BEACON_BLOCK_ROOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + ROOT_SIZE) {
414
488
  return null;
415
489
  }
416
490
 
417
- return getSlotFromOffset(data, SLOT_BYTES_POSITION_IN_SIGNED_DATA_COLUMN_SIDECAR);
491
+ blockRootBuf.set(
492
+ data.subarray(
493
+ BEACON_BLOCK_ROOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE,
494
+ BEACON_BLOCK_ROOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + ROOT_SIZE
495
+ )
496
+ );
497
+ return "0x" + blockRootBuf.toString("hex");
418
498
  }
419
499
 
420
500
  /**
@@ -1,15 +0,0 @@
1
- import { RootHex, Slot } from "@lodestar/types";
2
- /**
3
- * Cache to prevent processing multiple execution payload envelopes for the same block root.
4
- * Only one builder qualifies to submit an execution payload for a given slot.
5
- * We only keep track of envelopes of unfinalized slots.
6
- * [IGNORE] The node has not seen another valid `SignedExecutionPayloadEnvelope` for this block root.
7
- */
8
- export declare class SeenExecutionPayloadEnvelopes {
9
- private readonly slotByBlockRoot;
10
- private finalizedSlot;
11
- isKnown(blockRoot: RootHex): boolean;
12
- add(blockRoot: RootHex, slot: Slot): void;
13
- prune(finalizedSlot: Slot): void;
14
- }
15
- //# sourceMappingURL=seenExecutionPayloadEnvelope.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"seenExecutionPayloadEnvelope.d.ts","sourceRoot":"","sources":["../../../src/chain/seenCache/seenExecutionPayloadEnvelope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,OAAO,EAAE,IAAI,EAAC,MAAM,iBAAiB,CAAC;AAE9C;;;;;GAKG;AACH,qBAAa,6BAA6B;IACxC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA4B;IAC5D,OAAO,CAAC,aAAa,CAAW;IAEhC,OAAO,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAEnC;IAED,GAAG,CAAC,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAMxC;IAED,KAAK,CAAC,aAAa,EAAE,IAAI,GAAG,IAAI,CAQ/B;CACF"}