@lodestar/beacon-node 1.50.0-dev.3579e999b5 → 1.50.0-dev.408017a9db

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 (118) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +6 -1
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/beacon/pool/index.js +4 -4
  5. package/lib/api/impl/beacon/pool/index.js.map +1 -1
  6. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  7. package/lib/chain/blocks/importBlock.js +4 -0
  8. package/lib/chain/blocks/importBlock.js.map +1 -1
  9. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +2 -1
  10. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  11. package/lib/chain/chain.d.ts +5 -4
  12. package/lib/chain/chain.d.ts.map +1 -1
  13. package/lib/chain/chain.js +14 -16
  14. package/lib/chain/chain.js.map +1 -1
  15. package/lib/chain/errors/envelopeReconstructionError.d.ts +5 -0
  16. package/lib/chain/errors/envelopeReconstructionError.d.ts.map +1 -1
  17. package/lib/chain/errors/envelopeReconstructionError.js +2 -0
  18. package/lib/chain/errors/envelopeReconstructionError.js.map +1 -1
  19. package/lib/chain/errors/gossipValidation.d.ts +3 -1
  20. package/lib/chain/errors/gossipValidation.d.ts.map +1 -1
  21. package/lib/chain/errors/gossipValidation.js +2 -0
  22. package/lib/chain/errors/gossipValidation.js.map +1 -1
  23. package/lib/chain/initState.d.ts +2 -9
  24. package/lib/chain/initState.d.ts.map +1 -1
  25. package/lib/chain/initState.js +5 -38
  26. package/lib/chain/initState.js.map +1 -1
  27. package/lib/chain/interface.d.ts +2 -2
  28. package/lib/chain/interface.d.ts.map +1 -1
  29. package/lib/chain/opPools/opPool.d.ts +5 -4
  30. package/lib/chain/opPools/opPool.d.ts.map +1 -1
  31. package/lib/chain/opPools/opPool.js +62 -18
  32. package/lib/chain/opPools/opPool.js.map +1 -1
  33. package/lib/chain/opPools/utils.d.ts +4 -1
  34. package/lib/chain/opPools/utils.d.ts.map +1 -1
  35. package/lib/chain/opPools/utils.js +11 -1
  36. package/lib/chain/opPools/utils.js.map +1 -1
  37. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  38. package/lib/chain/prepareNextSlot.js +5 -2
  39. package/lib/chain/prepareNextSlot.js.map +1 -1
  40. package/lib/chain/produceBlock/produceBlockBody.d.ts +3 -1
  41. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  42. package/lib/chain/produceBlock/produceBlockBody.js +6 -2
  43. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  44. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +9 -2
  45. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  46. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +23 -13
  47. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  48. package/lib/chain/validation/attesterSlashing.d.ts +4 -4
  49. package/lib/chain/validation/attesterSlashing.d.ts.map +1 -1
  50. package/lib/chain/validation/attesterSlashing.js +3 -0
  51. package/lib/chain/validation/attesterSlashing.js.map +1 -1
  52. package/lib/chain/validation/proposerSlashing.d.ts +3 -3
  53. package/lib/chain/validation/proposerSlashing.d.ts.map +1 -1
  54. package/lib/chain/validation/proposerSlashing.js +3 -0
  55. package/lib/chain/validation/proposerSlashing.js.map +1 -1
  56. package/lib/db/repositories/proposerSlashing.d.ts +1 -0
  57. package/lib/db/repositories/proposerSlashing.d.ts.map +1 -1
  58. package/lib/db/repositories/proposerSlashing.js +5 -1
  59. package/lib/db/repositories/proposerSlashing.js.map +1 -1
  60. package/lib/index.d.ts +1 -1
  61. package/lib/index.d.ts.map +1 -1
  62. package/lib/index.js +1 -1
  63. package/lib/index.js.map +1 -1
  64. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  65. package/lib/network/processor/gossipHandlers.js +7 -4
  66. package/lib/network/processor/gossipHandlers.js.map +1 -1
  67. package/lib/network/processor/gossipValidatorFn.d.ts +7 -3
  68. package/lib/network/processor/gossipValidatorFn.d.ts.map +1 -1
  69. package/lib/network/processor/gossipValidatorFn.js +7 -3
  70. package/lib/network/processor/gossipValidatorFn.js.map +1 -1
  71. package/lib/network/processor/index.d.ts +9 -0
  72. package/lib/network/processor/index.d.ts.map +1 -1
  73. package/lib/network/processor/index.js +77 -10
  74. package/lib/network/processor/index.js.map +1 -1
  75. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js +1 -1
  76. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js.map +1 -1
  77. package/lib/sync/range/batch.d.ts +6 -0
  78. package/lib/sync/range/batch.d.ts.map +1 -1
  79. package/lib/sync/range/batch.js +10 -0
  80. package/lib/sync/range/batch.js.map +1 -1
  81. package/lib/sync/range/range.d.ts.map +1 -1
  82. package/lib/sync/range/range.js +50 -18
  83. package/lib/sync/range/range.js.map +1 -1
  84. package/lib/sync/unknownBlock.d.ts +9 -5
  85. package/lib/sync/unknownBlock.d.ts.map +1 -1
  86. package/lib/sync/unknownBlock.js +88 -46
  87. package/lib/sync/unknownBlock.js.map +1 -1
  88. package/lib/util/execution.d.ts +9 -11
  89. package/lib/util/execution.d.ts.map +1 -1
  90. package/lib/util/execution.js +5 -1
  91. package/lib/util/execution.js.map +1 -1
  92. package/package.json +15 -15
  93. package/src/api/impl/beacon/blocks/index.ts +7 -4
  94. package/src/api/impl/beacon/pool/index.ts +4 -4
  95. package/src/chain/blocks/importBlock.ts +4 -0
  96. package/src/chain/blocks/payloadEnvelopeInput/types.ts +2 -1
  97. package/src/chain/chain.ts +17 -19
  98. package/src/chain/errors/envelopeReconstructionError.ts +3 -0
  99. package/src/chain/errors/gossipValidation.ts +2 -0
  100. package/src/chain/initState.ts +8 -60
  101. package/src/chain/interface.ts +2 -2
  102. package/src/chain/opPools/opPool.ts +91 -18
  103. package/src/chain/opPools/utils.ts +31 -3
  104. package/src/chain/prepareNextSlot.ts +6 -3
  105. package/src/chain/produceBlock/produceBlockBody.ts +8 -1
  106. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +31 -13
  107. package/src/chain/validation/attesterSlashing.ts +7 -4
  108. package/src/chain/validation/proposerSlashing.ts +7 -4
  109. package/src/db/repositories/proposerSlashing.ts +6 -1
  110. package/src/index.ts +1 -1
  111. package/src/network/processor/gossipHandlers.ts +7 -4
  112. package/src/network/processor/gossipValidatorFn.ts +13 -3
  113. package/src/network/processor/index.ts +89 -10
  114. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts +1 -1
  115. package/src/sync/range/batch.ts +11 -0
  116. package/src/sync/range/range.ts +50 -18
  117. package/src/sync/unknownBlock.ts +99 -50
  118. package/src/util/execution.ts +16 -8
@@ -71,7 +71,7 @@ import {BufferPool} from "../util/bufferPool.js";
71
71
  import {Clock, ClockEvent, IClock} from "../util/clock.js";
72
72
  import {CustodyConfig, getValidatorsCustodyRequirement} from "../util/dataColumns.js";
73
73
  import {callInNextEventLoop} from "../util/eventLoop.js";
74
- import {ReconstructMismatchPolicy, isRebuildMiss, reconstructExecutionPayloadEnvelopes} from "../util/execution.js";
74
+ import {ReconstructMissPolicy, isRebuildMiss, reconstructExecutionPayloadEnvelopes} from "../util/execution.js";
75
75
  import {ensureDir, writeIfNotExist} from "../util/file.js";
76
76
  import {isOptimisticBlock} from "../util/forkChoice.js";
77
77
  import {JobItemQueue} from "../util/queue/itemQueue.js";
@@ -611,7 +611,7 @@ export class BeaconChain implements IBeaconChain {
611
611
  /** Populate in-memory caches with persisted data. Call at least once on startup */
612
612
  async loadFromDisk(): Promise<void> {
613
613
  await this.regen.init();
614
- await this.opPool.fromPersisted(this.db);
614
+ await this.opPool.fromPersisted(this.db, this.getHeadState(), this.bls, this.clock.currentSlot);
615
615
  }
616
616
 
617
617
  /** Persist in-memory data to the DB. Call at least once before stopping the process */
@@ -934,12 +934,13 @@ export class BeaconChain implements IBeaconChain {
934
934
 
935
935
  /**
936
936
  * Batch variant: archived header envelopes are rebuilt 32 per EL round-trip. Aligned with `requests`.
937
- * A body root mismatch is a local inconsistency: `"throw"` surfaces it, `"omit"` logs it and
938
- * returns null for that entry, for peer-facing paths where the spec allows omission.
937
+ * An archived envelope that cannot be rebuilt (EL-unavailable body or body root mismatch) is not the
938
+ * same as an unknown one: `"throw"` surfaces it, `"omit"` returns null for that entry, for
939
+ * peer-facing paths where the spec allows omission.
939
940
  */
940
941
  async getSerializedExecutionPayloadEnvelopes(
941
942
  requests: {blockSlot: Slot; blockRootHex: RootHex}[],
942
- onMismatch: ReconstructMismatchPolicy = "throw"
943
+ onMiss: ReconstructMissPolicy = "throw"
943
944
  ): Promise<(Uint8Array | null)[]> {
944
945
  const out: (Uint8Array | null)[] = new Array(requests.length).fill(null);
945
946
  const headerEnvelopes: gloas.SignedExecutionPayloadHeaderEnvelope[] = [];
@@ -978,14 +979,14 @@ export class BeaconChain implements IBeaconChain {
978
979
  for (let j = 0; j < rebuilt.length; j++) {
979
980
  const result = rebuilt[j];
980
981
  if (isRebuildMiss(result)) {
981
- if (result.reason === "mismatch") {
982
- if (onMismatch === "throw") throw result.error;
983
- this.logger.debug(
984
- "Archived envelope failed body root check against EL bodies",
985
- {slot: result.slot},
986
- result.error
987
- );
988
- }
982
+ if (onMiss === "throw") throw result.error;
983
+ this.logger.debug(
984
+ result.reason === "mismatch"
985
+ ? "Archived envelope failed body root check against EL bodies"
986
+ : "EL cannot serve bodies for archived envelope, omitting",
987
+ {slot: result.slot},
988
+ result.error
989
+ );
989
990
  continue;
990
991
  }
991
992
  out[headerEnvelopeIdxs[j]] = ssz.gloas.SignedExecutionPayloadEnvelope.serialize(result);
@@ -1016,10 +1017,7 @@ export class BeaconChain implements IBeaconChain {
1016
1017
  const [result] = await reconstructExecutionPayloadEnvelopes(this.executionEngine, this.metrics, [
1017
1018
  archived.headerEnvelope,
1018
1019
  ]);
1019
- if (isRebuildMiss(result)) {
1020
- if (result.reason === "mismatch") throw result.error;
1021
- return null;
1022
- }
1020
+ if (isRebuildMiss(result)) throw result.error;
1023
1021
  return result;
1024
1022
  }
1025
1023
 
@@ -1329,13 +1327,13 @@ export class BeaconChain implements IBeaconChain {
1329
1327
  };
1330
1328
 
1331
1329
  try {
1332
- await validateApiProposerSlashing(this, proposerSlashing);
1330
+ const verifiedDomain = await validateApiProposerSlashing(this, proposerSlashing);
1331
+ this.opPool.insertProposerSlashing(proposerSlashing, verifiedDomain);
1333
1332
  } catch (e) {
1334
1333
  this.logger.debug("Produced proposer slashing is not valid", {slot: blockSlot, proposerIndex}, e as Error);
1335
1334
  return;
1336
1335
  }
1337
1336
 
1338
- this.opPool.insertProposerSlashing(proposerSlashing);
1339
1337
  this.emitter.emit(routes.events.EventType.proposerSlashing, proposerSlashing);
1340
1338
  this.emitter.emit(ChainEvent.publishProposerSlashing, proposerSlashing);
1341
1339
  this.metrics?.opPool.proposerSlashingsProduced.inc();
@@ -4,6 +4,8 @@ import {LodestarError} from "@lodestar/utils";
4
4
  export enum EnvelopeReconstructionErrorCode {
5
5
  /** The EL failed to answer engine_getPayloadBodiesByHashV2 (transport / EL down). Transient. */
6
6
  ENGINE_UNAVAILABLE = "ENVELOPE_RECONSTRUCTION_ERROR_ENGINE_UNAVAILABLE",
7
+ /** The EL does not have the block or its block access list (pruned, or not yet re-indexed after a restart). May recover. */
8
+ BODY_UNAVAILABLE = "ENVELOPE_RECONSTRUCTION_ERROR_BODY_UNAVAILABLE",
7
9
  /** A body returned by the EL does not hash to the root stored in the header envelope. Local DB/EL inconsistency. */
8
10
  BODY_ROOT_MISMATCH = "ENVELOPE_RECONSTRUCTION_ERROR_BODY_ROOT_MISMATCH",
9
11
  /** A by-range stream stopped short at a slot that cannot be served; the response so far is still valid */
@@ -12,6 +14,7 @@ export enum EnvelopeReconstructionErrorCode {
12
14
 
13
15
  export type EnvelopeReconstructionErrorType =
14
16
  | {code: EnvelopeReconstructionErrorCode.ENGINE_UNAVAILABLE}
17
+ | {code: EnvelopeReconstructionErrorCode.BODY_UNAVAILABLE; slot: Slot}
15
18
  | {
16
19
  code: EnvelopeReconstructionErrorCode.BODY_ROOT_MISMATCH;
17
20
  slot: Slot;
@@ -9,6 +9,8 @@ export enum GossipErrorCode {
9
9
  INVALID_SERIALIZED_BYTES_ERROR_CODE = "GOSSIP_ERROR_INVALID_SERIALIZED_BYTES",
10
10
  PAST_SLOT = "GOSSIP_ERROR_PAST_SLOT",
11
11
  FUTURE_SLOT = "GOSSIP_ERROR_FUTURE_SLOT",
12
+ FATAL_PEER = "GOSSIP_ERROR_FATAL_PEER",
13
+ INVALID_SLOT = "GOSSIP_ERROR_INVALID_SLOT",
12
14
  }
13
15
 
14
16
  export class GossipActionError<T extends {code: string}> extends LodestarError<T> {
@@ -1,14 +1,8 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
2
  import {ForkPostGloas, ForkSeq, ZERO_HASH} from "@lodestar/params";
3
- import {
4
- BeaconStateAllForks,
5
- BeaconStateGloas,
6
- IBeaconStateView,
7
- computeEpochAtSlot,
8
- computeStartSlotAtEpoch,
9
- } from "@lodestar/state-transition";
3
+ import {BeaconStateAllForks, IBeaconStateView, computeEpochAtSlot, isStatePostGloas} from "@lodestar/state-transition";
10
4
  import {SignedBeaconBlock, ssz} from "@lodestar/types";
11
- import {Logger, byteArrayEquals, toHex, toRootHex} from "@lodestar/utils";
5
+ import {LodestarError, Logger, byteArrayEquals, toRootHex} from "@lodestar/utils";
12
6
  import {GENESIS_SLOT} from "../constants/index.js";
13
7
  import {IBeaconDb} from "../db/index.js";
14
8
  import {Metrics} from "../metrics/index.js";
@@ -17,7 +11,7 @@ import {getStateTypeFromBytes} from "../util/multifork.js";
17
11
  export async function persistAnchorState(
18
12
  config: ChainForkConfig,
19
13
  db: IBeaconDb,
20
- anchorState: BeaconStateAllForks,
14
+ anchorState: IBeaconStateView,
21
15
  anchorStateBytes: Uint8Array
22
16
  ): Promise<void> {
23
17
  if (anchorState.slot === GENESIS_SLOT) {
@@ -33,7 +27,8 @@ export async function persistAnchorState(
33
27
  const latestBlockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(latestBlockHeader);
34
28
 
35
29
  if (!byteArrayEquals(blockRoot, latestBlockRoot)) {
36
- throw Error(
30
+ throw new LodestarError(
31
+ {code: "GENESIS_BLOCK_ROOT_MISMATCH"},
37
32
  `Genesis block root ${toRootHex(blockRoot)} does not match genesis state latest block root ${toRootHex(latestBlockRoot)}`
38
33
  );
39
34
  }
@@ -48,16 +43,15 @@ export async function persistAnchorState(
48
43
  }
49
44
  }
50
45
 
51
- export function createGenesisBlock(config: ChainForkConfig, genesisState: BeaconStateAllForks): SignedBeaconBlock {
46
+ export function createGenesisBlock(config: ChainForkConfig, genesisState: IBeaconStateView): SignedBeaconBlock {
52
47
  const types = config.getForkTypes(GENESIS_SLOT);
53
48
  const genesisBlock = types.SignedBeaconBlock.defaultValue();
54
49
  const stateRoot = genesisState.hashTreeRoot();
55
50
  genesisBlock.message.stateRoot = stateRoot;
56
51
 
57
- if (config.getForkSeq(GENESIS_SLOT) >= ForkSeq.gloas) {
52
+ if (config.getForkSeq(GENESIS_SLOT) >= ForkSeq.gloas && isStatePostGloas(genesisState)) {
58
53
  const gloasBlock = genesisBlock as SignedBeaconBlock<ForkPostGloas>;
59
- const gloasState = genesisState as BeaconStateGloas;
60
- gloasBlock.message.body.signedExecutionPayloadBid.message = gloasState.latestExecutionPayloadBid.toValue();
54
+ gloasBlock.message.body.signedExecutionPayloadBid.message = genesisState.latestExecutionPayloadBid;
61
55
  }
62
56
 
63
57
  return genesisBlock;
@@ -86,52 +80,6 @@ export async function initStateFromDb(
86
80
  return state;
87
81
  }
88
82
 
89
- /**
90
- * Initialize and persist an anchor state (either weak subjectivity or genesis)
91
- */
92
- export async function checkAndPersistAnchorState(
93
- config: ChainForkConfig,
94
- db: IBeaconDb,
95
- logger: Logger,
96
- anchorState: BeaconStateAllForks,
97
- anchorStateBytes: Uint8Array,
98
- {
99
- isWithinWeakSubjectivityPeriod,
100
- isCheckpointState,
101
- }: {isWithinWeakSubjectivityPeriod: boolean; isCheckpointState: boolean}
102
- ): Promise<void> {
103
- const expectedFork = config.getForkInfo(computeStartSlotAtEpoch(anchorState.fork.epoch));
104
- const expectedForkVersion = toHex(expectedFork.version);
105
- const stateFork = toHex(anchorState.fork.currentVersion);
106
- if (stateFork !== expectedForkVersion) {
107
- throw Error(
108
- `State current fork version ${stateFork} not equal to current config ${expectedForkVersion}. Maybe caused by importing a state from a different network`
109
- );
110
- }
111
-
112
- const stateInfo = isCheckpointState ? "checkpoint" : "db";
113
- if (isWithinWeakSubjectivityPeriod) {
114
- logger.info(`Initializing beacon from a valid ${stateInfo} state`, {
115
- slot: anchorState.slot,
116
- epoch: computeEpochAtSlot(anchorState.slot),
117
- stateRoot: toRootHex(anchorState.hashTreeRoot()),
118
- isWithinWeakSubjectivityPeriod,
119
- });
120
- } else {
121
- logger.warn(`Initializing from a stale ${stateInfo} state vulnerable to long range attacks`, {
122
- slot: anchorState.slot,
123
- epoch: computeEpochAtSlot(anchorState.slot),
124
- stateRoot: toRootHex(anchorState.hashTreeRoot()),
125
- isWithinWeakSubjectivityPeriod,
126
- });
127
- logger.warn("Checkpoint sync recommended, please use --help to see checkpoint sync options");
128
- }
129
-
130
- if (isCheckpointState || anchorState.slot === GENESIS_SLOT) {
131
- await persistAnchorState(config, db, anchorState, anchorStateBytes);
132
- }
133
- }
134
-
135
83
  export function initBeaconMetrics(metrics: Metrics, state: IBeaconStateView): void {
136
84
  metrics.headSlot.set(state.slot);
137
85
  metrics.previousJustifiedEpoch.set(state.previousJustifiedCheckpoint.epoch);
@@ -30,7 +30,7 @@ import {Metrics} from "../metrics/metrics.js";
30
30
  import {BufferPool} from "../util/bufferPool.js";
31
31
  import {IClock} from "../util/clock.js";
32
32
  import {CustodyConfig} from "../util/dataColumns.js";
33
- import {ReconstructMismatchPolicy} from "../util/execution.js";
33
+ import {ReconstructMissPolicy} from "../util/execution.js";
34
34
  import {SerializedCache} from "../util/serializedCache.js";
35
35
  import {IArchiveStore} from "./archiveStore/interface.js";
36
36
  import {CheckpointBalancesCache} from "./balancesCache.js";
@@ -239,7 +239,7 @@ export interface IBeaconChain {
239
239
  getSerializedExecutionPayloadEnvelope(blockSlot: Slot, blockRootHex: string): Promise<Uint8Array | null>;
240
240
  getSerializedExecutionPayloadEnvelopes(
241
241
  requests: {blockSlot: Slot; blockRootHex: RootHex}[],
242
- onMismatch?: ReconstructMismatchPolicy
242
+ onMiss?: ReconstructMissPolicy
243
243
  ): Promise<(Uint8Array | null)[]>;
244
244
  getExecutionPayloadEnvelope(
245
245
  blockSlot: Slot,
@@ -12,38 +12,54 @@ import {
12
12
  } from "@lodestar/params";
13
13
  import {
14
14
  IBeaconStateView,
15
+ assertValidAttesterSlashing,
15
16
  computeEpochAtSlot,
16
17
  computeStartSlotAtEpoch,
17
18
  getAttesterSlashableIndices,
19
+ getAttesterSlashingSignatureSets,
20
+ getProposerSlashingSignatureSets,
18
21
  } from "@lodestar/state-transition";
19
22
  import {
20
23
  AttesterSlashing,
24
+ Domain,
21
25
  Epoch,
22
26
  SignedBeaconBlock,
27
+ Slot,
23
28
  ValidatorIndex,
24
29
  capella,
25
30
  phase0,
31
+ ssz,
26
32
  sszTypesFor,
27
33
  } from "@lodestar/types";
28
- import {fromHex, toHex, toRootHex} from "@lodestar/utils";
34
+ import {byteArrayEquals, fromHex, toHex, toRootHex} from "@lodestar/utils";
29
35
  import {IBeaconDb} from "../../db/index.js";
30
36
  import {Metrics} from "../../metrics/metrics.js";
31
37
  import {SignedBLSToExecutionChangeVersioned} from "../../util/types.js";
38
+ import {IBlsVerifier} from "../bls/interface.js";
32
39
  import {BlockType} from "../interface.js";
33
40
  import {BlockProductionStep} from "../produceBlock/produceBlockBody.js";
34
- import {isValidBlsToExecutionChangeForBlockInclusion} from "./utils.js";
41
+ import {
42
+ getAttesterSlashingSignatureDomains,
43
+ getProposerSlashingSignatureDomain,
44
+ isValidBlsToExecutionChangeForBlockInclusion,
45
+ } from "./utils.js";
35
46
 
36
47
  type HexRoot = string;
37
48
  type AttesterSlashingCached = {
38
49
  attesterSlashing: AttesterSlashing;
39
50
  intersectingIndices: number[];
51
+ verifiedDomains: [Domain, Domain];
52
+ };
53
+ type ProposerSlashingCached = {
54
+ proposerSlashing: phase0.ProposerSlashing;
55
+ verifiedDomain: Domain;
40
56
  };
41
57
 
42
58
  export class OpPool {
43
59
  /** Map of uniqueId(AttesterSlashing) -> AttesterSlashing */
44
60
  private readonly attesterSlashings = new Map<HexRoot, AttesterSlashingCached>();
45
61
  /** Map of to slash validator index -> ProposerSlashing */
46
- private readonly proposerSlashings = new Map<ValidatorIndex, phase0.ProposerSlashing>();
62
+ private readonly proposerSlashings = new Map<ValidatorIndex, ProposerSlashingCached>();
47
63
  /** Map of to exit validator index -> SignedVoluntaryExit */
48
64
  private readonly voluntaryExits = new Map<ValidatorIndex, phase0.SignedVoluntaryExit>();
49
65
  /** Set of seen attester slashing indexes. No need to prune */
@@ -68,7 +84,7 @@ export class OpPool {
68
84
  return this.blsToExecutionChanges.size;
69
85
  }
70
86
 
71
- async fromPersisted(db: IBeaconDb): Promise<void> {
87
+ async fromPersisted(db: IBeaconDb, state: IBeaconStateView, bls: IBlsVerifier, currentSlot: Slot): Promise<void> {
72
88
  const [attesterSlashings, proposerSlashings, voluntaryExits, blsToExecutionChanges] = await Promise.all([
73
89
  db.attesterSlashing.entries(),
74
90
  db.proposerSlashing.values(),
@@ -76,11 +92,42 @@ export class OpPool {
76
92
  db.blsToExecutionChange.values(),
77
93
  ]);
78
94
 
79
- for (const attesterSlashing of attesterSlashings) {
80
- this.insertAttesterSlashing(ForkName.electra, attesterSlashing.value, attesterSlashing.key);
95
+ // Persisted operations have no verification context. Rebuild it before they can enter the pool.
96
+ // The startup anchor may precede a fork whose slashings are already persisted.
97
+ for (const {key, value: attesterSlashing} of attesterSlashings) {
98
+ try {
99
+ assertValidAttesterSlashing(this.config, currentSlot, state.validatorCount, attesterSlashing, false);
100
+ } catch {
101
+ continue;
102
+ }
103
+ const verifiedDomains = getAttesterSlashingSignatureDomains(this.config, currentSlot, attesterSlashing);
104
+ if (
105
+ !(await bls.verifySignatureSets(getAttesterSlashingSignatureSets(this.config, currentSlot, attesterSlashing)))
106
+ ) {
107
+ continue;
108
+ }
109
+ this.insertAttesterSlashing(ForkName.electra, attesterSlashing, verifiedDomains, key);
81
110
  }
82
111
  for (const proposerSlashing of proposerSlashings) {
83
- this.insertProposerSlashing(proposerSlashing);
112
+ const header1 = proposerSlashing.signedHeader1.message;
113
+ const header2 = proposerSlashing.signedHeader2.message;
114
+ // Startup may restore a state from before activation. Current slashability is checked
115
+ // when selecting operations, so it must not prevent restoring an otherwise valid slashing.
116
+ if (
117
+ header1.slot !== header2.slot ||
118
+ header1.proposerIndex !== header2.proposerIndex ||
119
+ header1.proposerIndex >= state.validatorCount ||
120
+ ssz.phase0.BeaconBlockHeaderBigint.equals(header1, header2)
121
+ ) {
122
+ continue;
123
+ }
124
+ const verifiedDomain = getProposerSlashingSignatureDomain(this.config, currentSlot, proposerSlashing);
125
+ if (
126
+ !(await bls.verifySignatureSets(getProposerSlashingSignatureSets(this.config, currentSlot, proposerSlashing)))
127
+ ) {
128
+ continue;
129
+ }
130
+ this.insertProposerSlashing(proposerSlashing, verifiedDomain);
84
131
  }
85
132
  for (const voluntaryExit of voluntaryExits) {
86
133
  this.insertVoluntaryExit(voluntaryExit);
@@ -102,8 +149,12 @@ export class OpPool {
102
149
  ),
103
150
  persistDiff(
104
151
  db.proposerSlashing,
105
- Array.from(this.proposerSlashings.entries()).map(([key, value]) => ({key, value})),
106
- (index) => index
152
+ Array.from(this.proposerSlashings.entries()).map(([key, value]) => ({key, value: value.proposerSlashing})),
153
+ (index) => index,
154
+ // A slashing invalidated by a fork may remain on disk after eviction from the pool.
155
+ // A new valid slashing for that validator has the same database key, so comparing
156
+ // keys alone would retain the invalid slashing and lose the replacement on restart.
157
+ {updateExisting: true}
107
158
  ),
108
159
  persistDiff(
109
160
  db.voluntaryExit,
@@ -143,7 +194,12 @@ export class OpPool {
143
194
  }
144
195
 
145
196
  /** Must be validated beforehand */
146
- insertAttesterSlashing(fork: ForkName, attesterSlashing: AttesterSlashing, rootHash?: Uint8Array): void {
197
+ insertAttesterSlashing(
198
+ fork: ForkName,
199
+ attesterSlashing: AttesterSlashing,
200
+ verifiedDomains: [Domain, Domain],
201
+ rootHash?: Uint8Array
202
+ ): void {
147
203
  if (!rootHash) {
148
204
  rootHash = sszTypesFor(fork).AttesterSlashing.hashTreeRoot(attesterSlashing);
149
205
  }
@@ -153,6 +209,7 @@ export class OpPool {
153
209
  this.attesterSlashings.set(toRootHex(rootHash), {
154
210
  attesterSlashing,
155
211
  intersectingIndices,
212
+ verifiedDomains,
156
213
  });
157
214
  for (const index of intersectingIndices) {
158
215
  this.attesterSlashingIndexes.add(index);
@@ -160,8 +217,11 @@ export class OpPool {
160
217
  }
161
218
 
162
219
  /** Must be validated beforehand */
163
- insertProposerSlashing(proposerSlashing: phase0.ProposerSlashing): void {
164
- this.proposerSlashings.set(proposerSlashing.signedHeader1.message.proposerIndex, proposerSlashing);
220
+ insertProposerSlashing(proposerSlashing: phase0.ProposerSlashing, verifiedDomain: Domain): void {
221
+ this.proposerSlashings.set(proposerSlashing.signedHeader1.message.proposerIndex, {
222
+ proposerSlashing,
223
+ verifiedDomain,
224
+ });
165
225
  }
166
226
 
167
227
  /** Must be validated beforehand */
@@ -204,7 +264,14 @@ export class OpPool {
204
264
  : metrics?.builderBlockProductionTimeSteps;
205
265
 
206
266
  const endProposerSlashing = stepsMetrics?.startTimer();
207
- for (const proposerSlashing of this.proposerSlashings.values()) {
267
+ for (const [key, {proposerSlashing, verifiedDomain}] of this.proposerSlashings) {
268
+ // A fork can change the required domain even for verified slashings, including those with future-slot headers.
269
+ if (
270
+ !byteArrayEquals(verifiedDomain, getProposerSlashingSignatureDomain(this.config, state.slot, proposerSlashing))
271
+ ) {
272
+ this.proposerSlashings.delete(key);
273
+ continue;
274
+ }
208
275
  const index = proposerSlashing.signedHeader1.message.proposerIndex;
209
276
  const validator = state.getValidator(index);
210
277
  if (!validator.slashed && validator.activationEpoch <= stateEpoch && stateEpoch < validator.withdrawableEpoch) {
@@ -223,7 +290,12 @@ export class OpPool {
223
290
  const endAttesterSlashings = stepsMetrics?.startTimer();
224
291
  const attesterSlashings: AttesterSlashing[] = [];
225
292
  const maxAttesterSlashings = stateFork >= ForkSeq.electra ? MAX_ATTESTER_SLASHINGS_ELECTRA : MAX_ATTESTER_SLASHINGS;
226
- attesterSlashing: for (const attesterSlashing of this.attesterSlashings.values()) {
293
+ attesterSlashing: for (const [key, attesterSlashing] of this.attesterSlashings) {
294
+ const domains = getAttesterSlashingSignatureDomains(this.config, state.slot, attesterSlashing.attesterSlashing);
295
+ if (!attesterSlashing.verifiedDomains.every((domain, i) => byteArrayEquals(domain, domains[i]))) {
296
+ this.attesterSlashings.delete(key);
297
+ continue;
298
+ }
227
299
  /** Indices slashable in this attester slashing */
228
300
  const slashableIndices = new Set<ValidatorIndex>();
229
301
  for (let i = 0; i < attesterSlashing.intersectingIndices.length; i++) {
@@ -303,7 +375,7 @@ export class OpPool {
303
375
 
304
376
  /** For beacon pool API */
305
377
  getAllProposerSlashings(): phase0.ProposerSlashing[] {
306
- return Array.from(this.proposerSlashings.values());
378
+ return Array.from(this.proposerSlashings.values()).map(({proposerSlashing}) => proposerSlashing);
307
379
  }
308
380
 
309
381
  /** For beacon pool API */
@@ -356,7 +428,7 @@ export class OpPool {
356
428
  */
357
429
  private pruneProposerSlashings(headState: IBeaconStateView): void {
358
430
  const finalizedEpoch = headState.finalizedCheckpoint.epoch;
359
- for (const [key, proposerSlashing] of this.proposerSlashings.entries()) {
431
+ for (const [key, {proposerSlashing}] of this.proposerSlashings.entries()) {
360
432
  const index = proposerSlashing.signedHeader1.message.proposerIndex;
361
433
  if (headState.getValidator(index).exitEpoch <= finalizedEpoch) {
362
434
  this.proposerSlashings.delete(key);
@@ -436,14 +508,15 @@ function isSlashableAtEpoch(validator: phase0.Validator, epoch: Epoch): boolean
436
508
  async function persistDiff<K extends Id, V>(
437
509
  dbRepo: Repository<K, V>,
438
510
  items: {key: K; value: V}[],
439
- serializeKey: (key: K) => number | string
511
+ serializeKey: (key: K) => number | string,
512
+ opts: {updateExisting?: boolean} = {}
440
513
  ): Promise<void> {
441
514
  const persistedKeys = await dbRepo.keys();
442
515
  const batch: DbBatch<K, V> = [];
443
516
 
444
517
  const persistedKeysSerialized = new Set(persistedKeys.map(serializeKey));
445
518
  for (const item of items) {
446
- if (!persistedKeysSerialized.has(serializeKey(item.key))) {
519
+ if (opts.updateExisting || !persistedKeysSerialized.has(serializeKey(item.key))) {
447
520
  batch.push({type: "put", key: item.key, value: item.value});
448
521
  }
449
522
  }
@@ -1,9 +1,37 @@
1
1
  import {Signature} from "@chainsafe/lodestar-z/blst";
2
- import {BLS_WITHDRAWAL_PREFIX} from "@lodestar/params";
3
- import {IBeaconStateView} from "@lodestar/state-transition";
4
- import {Slot, capella} from "@lodestar/types";
2
+ import {BeaconConfig} from "@lodestar/config";
3
+ import {BLS_WITHDRAWAL_PREFIX, DOMAIN_BEACON_ATTESTER, DOMAIN_BEACON_PROPOSER} from "@lodestar/params";
4
+ import {IBeaconStateView, computeStartSlotAtEpoch} from "@lodestar/state-transition";
5
+ import {AttesterSlashing, Domain, Slot, capella, phase0} from "@lodestar/types";
5
6
  import {AggregateFast, AggregateFastElectra} from "./attestationPool.js";
6
7
 
8
+ export function getProposerSlashingSignatureDomain(
9
+ config: BeaconConfig,
10
+ stateSlot: Slot,
11
+ slashing: phase0.ProposerSlashing
12
+ ): Domain {
13
+ return config.getDomain(stateSlot, DOMAIN_BEACON_PROPOSER, Number(slashing.signedHeader1.message.slot));
14
+ }
15
+
16
+ export function getAttesterSlashingSignatureDomains(
17
+ config: BeaconConfig,
18
+ stateSlot: Slot,
19
+ slashing: AttesterSlashing
20
+ ): [Domain, Domain] {
21
+ return [
22
+ config.getDomain(
23
+ stateSlot,
24
+ DOMAIN_BEACON_ATTESTER,
25
+ computeStartSlotAtEpoch(Number(slashing.attestation1.data.target.epoch))
26
+ ),
27
+ config.getDomain(
28
+ stateSlot,
29
+ DOMAIN_BEACON_ATTESTER,
30
+ computeStartSlotAtEpoch(Number(slashing.attestation2.data.target.epoch))
31
+ ),
32
+ ];
33
+ }
34
+
7
35
  /**
8
36
  * Prune a Map indexed by slot to keep the most recent slots, up to `slotsRetained`
9
37
  */
@@ -244,6 +244,10 @@ export class PrepareNextSlotScheduler {
244
244
  parentBlockHash = preparedState.latestExecutionPayloadHeader.blockHash;
245
245
  }
246
246
 
247
+ // The payload_attributes event must carry the same hashes we send to the EL
248
+ const safeBlockHash = getSafeExecutionBlockHash(this.chain.forkChoice, this.logger);
249
+ const finalizedBlockHash = getFinalizedExecutionBlockHash(this.chain.forkChoice);
250
+
247
251
  let payloadAttributes: PayloadAttributes | undefined;
248
252
  // If emitPayloadAttributes is true emit a SSE payloadAttributes event for
249
253
  // every slot. Without the flag, only emit the event if we are proposing in the next slot.
@@ -256,6 +260,8 @@ export class PrepareNextSlotScheduler {
256
260
  prepareSlot,
257
261
  parentBlockRoot: fromHex(updatedHead.blockRoot),
258
262
  parentBlockHash,
263
+ safeBlockHash,
264
+ finalizedBlockHash,
259
265
  feeRecipient: feeRecipient ?? "0x0000000000000000000000000000000000000000",
260
266
  });
261
267
  this.chain.emitter.emit(routes.events.EventType.payloadAttributes, {data, version: fork});
@@ -267,9 +273,6 @@ export class PrepareNextSlotScheduler {
267
273
  computeTimeAtSlot(this.config, prepareSlot, this.chain.genesisTime) - Date.now() / 1000;
268
274
  this.metrics?.blockPayload.payloadAdvancePrepTime.observe(preparationTime);
269
275
 
270
- const safeBlockHash = getSafeExecutionBlockHash(this.chain.forkChoice, this.logger);
271
- const finalizedBlockHash = getFinalizedExecutionBlockHash(this.chain.forkChoice);
272
-
273
276
  // awaiting here instead of throwing an async call because there is no other task
274
277
  // left for scheduler and this gives nice semantics to catch and log errors in the
275
278
  // try/catch wrapper here.
@@ -845,6 +845,8 @@ export function getPayloadAttributesForSSE(
845
845
  prepareSlot,
846
846
  parentBlockRoot,
847
847
  parentBlockHash,
848
+ safeBlockHash,
849
+ finalizedBlockHash,
848
850
  feeRecipient,
849
851
  }: {
850
852
  /**
@@ -855,6 +857,8 @@ export function getPayloadAttributesForSSE(
855
857
  prepareSlot: Slot;
856
858
  parentBlockRoot: Root;
857
859
  parentBlockHash: Bytes32;
860
+ safeBlockHash: RootHex;
861
+ finalizedBlockHash: RootHex;
858
862
  feeRecipient: string;
859
863
  }
860
864
  ): SSEPayloadAttributes {
@@ -874,7 +878,10 @@ export function getPayloadAttributesForSSE(
874
878
  payloadAttributes,
875
879
  } as SSEPayloadAttributes;
876
880
 
877
- if (!isForkPostGloas(fork)) {
881
+ if (isForkPostGloas(fork)) {
882
+ (ssePayloadAttributes as gloas.SSEPayloadAttributes).safeBlockHash = fromHex(safeBlockHash);
883
+ (ssePayloadAttributes as gloas.SSEPayloadAttributes).finalizedBlockHash = fromHex(finalizedBlockHash);
884
+ } else {
878
885
  // Removed in Gloas, builders can get the block number from the EL via the block hash if required
879
886
  (ssePayloadAttributes as bellatrix.SSEPayloadAttributes).parentBlockNumber = prepareState.payloadBlockNumber;
880
887
  }
@@ -47,8 +47,9 @@ export type SeenPayloadEnvelopeInputModules = {
47
47
  * Created whenever we have a block because it needs block bid.
48
48
  * Steady state (linear chain, healthy progression): the cache holds ~2 entries — the head
49
49
  * (parent for next-slot production) and its parent (proposer-boost-reorg fallback). It can
50
- * transiently hold more during forks, range-sync bursts, or when `prepareNextSlot` skips
51
- * ticks; subsequent ticks settle it back.
50
+ * hold more during forks and range sync, or while an entry's payload is not FULL on the head's
51
+ * branch or its columns are still being gathered. Such entries are evicted on a later block
52
+ * import, on finalization or by the size cap.
52
53
  */
53
54
  export class SeenPayloadEnvelopeInput {
54
55
  private readonly config: ChainForkConfig;
@@ -264,18 +265,35 @@ export class SeenPayloadEnvelopeInput {
264
265
  }
265
266
  }
266
267
 
268
+ /**
269
+ * Removes a PayloadEnvelopeInput whose envelope failed import with a definitive invalid verdict. The envelope
270
+ * cannot be replaced in place, sync recreates the entry via `getOrReload()` or `add()` to fetch another one.
271
+ * No-op if the cached entry is a different object, it may already be the recreated one.
272
+ */
273
+ removeInvalid(payloadInput: PayloadEnvelopeInput): void {
274
+ if (this.payloadInputs.get(payloadInput.blockRootHex) === payloadInput) {
275
+ this.evictPayloadInput(payloadInput, "invalid");
276
+ }
277
+ }
278
+
267
279
  pruneBelowParent(parentBlock: ProtoBlock): void {
268
- for (const block of this.forkChoice.getAllAncestorBlocks(parentBlock.blockRoot, parentBlock.payloadStatus)) {
269
- // Only evict once the payload is FULL (revealed/imported) — on an EMPTY/PENDING branch we may
270
- // still need to download the FULL envelope (see #9475), and evicting would make payload-by-root
271
- // sync throw "Missing PayloadEnvelopeInput for known block".
272
- if (block.slot < parentBlock.slot && block.payloadStatus === PayloadStatus.FULL) {
273
- const input = this.payloadInputs.get(block.blockRoot);
274
- // ...and don't evict while columns are still being gathered: writeDataColumnsToDb awaits the
275
- // same hasComputedAllData() before persisting. Such entries are pruned by a later call.
276
- if (input?.hasComputedAllData()) {
277
- this.evictPayloadInput(input, "belowParent");
278
- }
280
+ for (const input of this.payloadInputs.values()) {
281
+ if (
282
+ input.slot < parentBlock.slot &&
283
+ // Don't evict while columns are still being gathered: writeDataColumnsToDb awaits the same
284
+ // hasComputedAllData() before persisting. Such entries are pruned by a later call.
285
+ input.hasComputedAllData() &&
286
+ // Only evict once the payload is FULL (revealed/imported) on the parent's branch. On an EMPTY/PENDING
287
+ // branch we may still need to download the FULL envelope (see #9475), and without the entry gossip
288
+ // for the block is ignored until by-root sync reloads it.
289
+ this.forkChoice.isDescendant(
290
+ input.blockRootHex,
291
+ PayloadStatus.FULL,
292
+ parentBlock.blockRoot,
293
+ parentBlock.payloadStatus
294
+ )
295
+ ) {
296
+ this.evictPayloadInput(input, "belowParent");
279
297
  }
280
298
  }
281
299
  }