@lodestar/state-transition 1.43.0 → 1.44.0-dev.055b83cb3d

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 (82) hide show
  1. package/lib/block/processDepositRequest.d.ts +3 -11
  2. package/lib/block/processDepositRequest.d.ts.map +1 -1
  3. package/lib/block/processDepositRequest.js +27 -35
  4. package/lib/block/processDepositRequest.js.map +1 -1
  5. package/lib/block/processParentExecutionPayload.d.ts.map +1 -1
  6. package/lib/block/processParentExecutionPayload.js +4 -3
  7. package/lib/block/processParentExecutionPayload.js.map +1 -1
  8. package/lib/cache/epochCache.d.ts.map +1 -1
  9. package/lib/cache/epochCache.js +10 -7
  10. package/lib/cache/epochCache.js.map +1 -1
  11. package/lib/lightClient/spec/index.d.ts +22 -0
  12. package/lib/lightClient/spec/index.d.ts.map +1 -0
  13. package/lib/lightClient/spec/index.js +58 -0
  14. package/lib/lightClient/spec/index.js.map +1 -0
  15. package/lib/lightClient/spec/isBetterUpdate.d.ts +23 -0
  16. package/lib/lightClient/spec/isBetterUpdate.d.ts.map +1 -0
  17. package/lib/lightClient/spec/isBetterUpdate.js +66 -0
  18. package/lib/lightClient/spec/isBetterUpdate.js.map +1 -0
  19. package/lib/lightClient/spec/processLightClientUpdate.d.ts +12 -0
  20. package/lib/lightClient/spec/processLightClientUpdate.d.ts.map +1 -0
  21. package/lib/lightClient/spec/processLightClientUpdate.js +80 -0
  22. package/lib/lightClient/spec/processLightClientUpdate.js.map +1 -0
  23. package/lib/lightClient/spec/store.d.ts +45 -0
  24. package/lib/lightClient/spec/store.d.ts.map +1 -0
  25. package/lib/lightClient/spec/store.js +56 -0
  26. package/lib/lightClient/spec/store.js.map +1 -0
  27. package/lib/lightClient/spec/utils.d.ts +47 -0
  28. package/lib/lightClient/spec/utils.d.ts.map +1 -0
  29. package/lib/lightClient/spec/utils.js +197 -0
  30. package/lib/lightClient/spec/utils.js.map +1 -0
  31. package/lib/lightClient/spec/validateLightClientBootstrap.d.ts +4 -0
  32. package/lib/lightClient/spec/validateLightClientBootstrap.d.ts.map +1 -0
  33. package/lib/lightClient/spec/validateLightClientBootstrap.js +22 -0
  34. package/lib/lightClient/spec/validateLightClientBootstrap.js.map +1 -0
  35. package/lib/lightClient/spec/validateLightClientUpdate.d.ts +5 -0
  36. package/lib/lightClient/spec/validateLightClientUpdate.d.ts.map +1 -0
  37. package/lib/lightClient/spec/validateLightClientUpdate.js +88 -0
  38. package/lib/lightClient/spec/validateLightClientUpdate.js.map +1 -0
  39. package/lib/slot/upgradeStateToGloas.d.ts.map +1 -1
  40. package/lib/slot/upgradeStateToGloas.js +35 -29
  41. package/lib/slot/upgradeStateToGloas.js.map +1 -1
  42. package/lib/stateView/beaconStateView.d.ts +9 -3
  43. package/lib/stateView/beaconStateView.d.ts.map +1 -1
  44. package/lib/stateView/beaconStateView.js +23 -4
  45. package/lib/stateView/beaconStateView.js.map +1 -1
  46. package/lib/stateView/interface.d.ts +2 -1
  47. package/lib/stateView/interface.d.ts.map +1 -1
  48. package/lib/stateView/interface.js.map +1 -1
  49. package/lib/util/gloas.d.ts +14 -0
  50. package/lib/util/gloas.d.ts.map +1 -1
  51. package/lib/util/gloas.js +24 -0
  52. package/lib/util/gloas.js.map +1 -1
  53. package/lib/util/index.d.ts +1 -0
  54. package/lib/util/index.d.ts.map +1 -1
  55. package/lib/util/index.js +1 -0
  56. package/lib/util/index.js.map +1 -1
  57. package/lib/util/pendingDepositsLookup.d.ts +40 -0
  58. package/lib/util/pendingDepositsLookup.d.ts.map +1 -0
  59. package/lib/util/pendingDepositsLookup.js +84 -0
  60. package/lib/util/pendingDepositsLookup.js.map +1 -0
  61. package/lib/util/shuffling.d.ts +6 -5
  62. package/lib/util/shuffling.d.ts.map +1 -1
  63. package/lib/util/shuffling.js +13 -15
  64. package/lib/util/shuffling.js.map +1 -1
  65. package/package.json +12 -7
  66. package/src/block/processDepositRequest.ts +29 -47
  67. package/src/block/processParentExecutionPayload.ts +4 -3
  68. package/src/cache/epochCache.ts +10 -7
  69. package/src/lightClient/spec/index.ts +101 -0
  70. package/src/lightClient/spec/isBetterUpdate.ts +94 -0
  71. package/src/lightClient/spec/processLightClientUpdate.ts +119 -0
  72. package/src/lightClient/spec/store.ts +106 -0
  73. package/src/lightClient/spec/utils.ts +317 -0
  74. package/src/lightClient/spec/validateLightClientBootstrap.ts +39 -0
  75. package/src/lightClient/spec/validateLightClientUpdate.ts +145 -0
  76. package/src/slot/upgradeStateToGloas.ts +43 -45
  77. package/src/stateView/beaconStateView.ts +23 -4
  78. package/src/stateView/interface.ts +2 -1
  79. package/src/util/gloas.ts +28 -0
  80. package/src/util/index.ts +1 -0
  81. package/src/util/pendingDepositsLookup.ts +105 -0
  82. package/src/util/shuffling.ts +17 -15
@@ -0,0 +1,145 @@
1
+ import {PublicKey, Signature, fastAggregateVerify} from "@chainsafe/blst";
2
+ import {ChainForkConfig} from "@lodestar/config";
3
+ import {
4
+ DOMAIN_SYNC_COMMITTEE,
5
+ FINALIZED_ROOT_DEPTH,
6
+ FINALIZED_ROOT_DEPTH_ELECTRA,
7
+ FINALIZED_ROOT_INDEX,
8
+ FINALIZED_ROOT_INDEX_ELECTRA,
9
+ GENESIS_SLOT,
10
+ MIN_SYNC_COMMITTEE_PARTICIPANTS,
11
+ NEXT_SYNC_COMMITTEE_DEPTH,
12
+ NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA,
13
+ NEXT_SYNC_COMMITTEE_INDEX,
14
+ NEXT_SYNC_COMMITTEE_INDEX_ELECTRA,
15
+ } from "@lodestar/params";
16
+ import {LightClientUpdate, Root, isElectraLightClientUpdate, ssz} from "@lodestar/types";
17
+ import type {ILightClientStore, SyncCommitteeFast} from "./store.js";
18
+ import {
19
+ ZERO_HASH,
20
+ getParticipantPubkeys,
21
+ isFinalityUpdate,
22
+ isSyncCommitteeUpdate,
23
+ isValidLightClientHeader,
24
+ isValidMerkleBranch,
25
+ isZeroedHeader,
26
+ isZeroedSyncCommittee,
27
+ sumBits,
28
+ } from "./utils.js";
29
+
30
+ export function validateLightClientUpdate(
31
+ config: ChainForkConfig,
32
+ store: ILightClientStore,
33
+ update: LightClientUpdate,
34
+ syncCommittee: SyncCommitteeFast
35
+ ): void {
36
+ // Verify sync committee has sufficient participants
37
+ if (sumBits(update.syncAggregate.syncCommitteeBits) < MIN_SYNC_COMMITTEE_PARTICIPANTS) {
38
+ throw Error("Sync committee has not sufficient participants");
39
+ }
40
+
41
+ if (!isValidLightClientHeader(config, update.attestedHeader)) {
42
+ throw Error("Attested Header is not Valid Light Client Header");
43
+ }
44
+
45
+ // Sanity check that slots are in correct order
46
+ if (update.signatureSlot <= update.attestedHeader.beacon.slot) {
47
+ throw Error(
48
+ `signature slot ${update.signatureSlot} must be after attested header slot ${update.attestedHeader.beacon.slot}`
49
+ );
50
+ }
51
+ if (update.attestedHeader.beacon.slot < update.finalizedHeader.beacon.slot) {
52
+ throw Error(
53
+ `attested header slot ${update.signatureSlot} must be after finalized header slot ${update.finalizedHeader.beacon.slot}`
54
+ );
55
+ }
56
+
57
+ // Verify that the `finality_branch`, if present, confirms `finalized_header`
58
+ // to match the finalized checkpoint root saved in the state of `attested_header`.
59
+ // Note that the genesis finalized checkpoint root is represented as a zero hash.
60
+ if (!isFinalityUpdate(update)) {
61
+ if (!isZeroedHeader(update.finalizedHeader.beacon)) {
62
+ throw Error("finalizedHeader must be zero for non-finality update");
63
+ }
64
+ } else {
65
+ let finalizedRoot: Root;
66
+
67
+ if (update.finalizedHeader.beacon.slot === GENESIS_SLOT) {
68
+ if (!isZeroedHeader(update.finalizedHeader.beacon)) {
69
+ throw Error("finalizedHeader must be zero for not finality update");
70
+ }
71
+ finalizedRoot = ZERO_HASH;
72
+ } else {
73
+ if (!isValidLightClientHeader(config, update.finalizedHeader)) {
74
+ throw Error("Finalized Header is not valid Light Client Header");
75
+ }
76
+
77
+ finalizedRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(update.finalizedHeader.beacon);
78
+ }
79
+
80
+ if (
81
+ !isValidMerkleBranch(
82
+ finalizedRoot,
83
+ update.finalityBranch,
84
+ isElectraLightClientUpdate(update) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH,
85
+ isElectraLightClientUpdate(update) ? FINALIZED_ROOT_INDEX_ELECTRA : FINALIZED_ROOT_INDEX,
86
+ update.attestedHeader.beacon.stateRoot
87
+ )
88
+ ) {
89
+ throw Error("Invalid finality header merkle branch");
90
+ }
91
+ }
92
+
93
+ // Verify that the `next_sync_committee`, if present, actually is the next sync committee saved in the
94
+ // state of the `attested_header`
95
+ if (!isSyncCommitteeUpdate(update)) {
96
+ if (!isZeroedSyncCommittee(update.nextSyncCommittee)) {
97
+ throw Error("nextSyncCommittee must be zero for non sync committee update");
98
+ }
99
+ } else {
100
+ if (
101
+ !isValidMerkleBranch(
102
+ ssz.altair.SyncCommittee.hashTreeRoot(update.nextSyncCommittee),
103
+ update.nextSyncCommitteeBranch,
104
+ isElectraLightClientUpdate(update) ? NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA : NEXT_SYNC_COMMITTEE_DEPTH,
105
+ isElectraLightClientUpdate(update) ? NEXT_SYNC_COMMITTEE_INDEX_ELECTRA : NEXT_SYNC_COMMITTEE_INDEX,
106
+ update.attestedHeader.beacon.stateRoot
107
+ )
108
+ ) {
109
+ throw Error("Invalid next sync committee merkle branch");
110
+ }
111
+ }
112
+
113
+ // Verify sync committee aggregate signature
114
+
115
+ const participantPubkeys = getParticipantPubkeys(syncCommittee.pubkeys, update.syncAggregate.syncCommitteeBits);
116
+
117
+ const signingRoot = ssz.phase0.SigningData.hashTreeRoot({
118
+ objectRoot: ssz.phase0.BeaconBlockHeader.hashTreeRoot(update.attestedHeader.beacon),
119
+ domain: store.config.getDomain(update.signatureSlot - 1, DOMAIN_SYNC_COMMITTEE),
120
+ });
121
+
122
+ if (!isValidBlsAggregate(participantPubkeys, signingRoot, update.syncAggregate.syncCommitteeSignature)) {
123
+ throw Error("Invalid aggregate signature");
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Same as BLS.verifyAggregate but with detailed error messages
129
+ */
130
+ function isValidBlsAggregate(publicKeys: PublicKey[], message: Uint8Array, signature: Uint8Array): boolean {
131
+ let sig: Signature;
132
+ try {
133
+ sig = Signature.fromBytes(signature, true);
134
+ } catch (e) {
135
+ (e as Error).message = `Error deserializing signature: ${(e as Error).message}`;
136
+ throw e;
137
+ }
138
+
139
+ try {
140
+ return fastAggregateVerify(message, publicKeys, sig);
141
+ } catch (e) {
142
+ (e as Error).message = `Error verifying signature: ${(e as Error).message}`;
143
+ throw e;
144
+ }
145
+ }
@@ -1,12 +1,12 @@
1
1
  import {SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
2
2
  import {ssz} from "@lodestar/types";
3
- import {toHex} from "@lodestar/utils";
4
- import {isValidDepositSignature} from "../block/processDeposit.js";
3
+ import {toPubkeyHex} from "@lodestar/utils";
5
4
  import {applyDepositForBuilder} from "../block/processDepositRequest.js";
6
5
  import {getCachedBeaconState} from "../cache/stateCache.js";
7
6
  import {CachedBeaconStateFulu, CachedBeaconStateGloas} from "../types.js";
8
7
  import {initializePtcWindow, isBuilderWithdrawalCredential} from "../util/gloas.js";
9
8
  import {isValidatorKnown} from "../util/index.js";
9
+ import {PendingDepositsLookup} from "../util/pendingDepositsLookup.js";
10
10
 
11
11
  /**
12
12
  * Upgrade a state from Fulu to Gloas.
@@ -48,6 +48,7 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea
48
48
  stateGloasView.currentSyncCommittee = stateGloasCloned.currentSyncCommittee;
49
49
  stateGloasView.nextSyncCommittee = stateGloasCloned.nextSyncCommittee;
50
50
  stateGloasView.latestExecutionPayloadBid.blockHash = stateFulu.latestExecutionPayloadHeader.blockHash;
51
+ stateGloasView.latestExecutionPayloadBid.gasLimit = BigInt(stateFulu.latestExecutionPayloadHeader.gasLimit);
51
52
  stateGloasView.latestExecutionPayloadBid.executionRequestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot(
52
53
  ssz.electra.ExecutionRequests.defaultValue()
53
54
  );
@@ -86,66 +87,63 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea
86
87
 
87
88
  /**
88
89
  * Applies any pending deposits for builders to onboard builders during the fork transition
89
- * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.2/specs/gloas/fork.md#new-onboard_builders_from_pending_deposits
90
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.8/specs/gloas/fork.md#new-onboard_builders_from_pending_deposits
90
91
  */
91
92
  function onboardBuildersFromPendingDeposits(state: CachedBeaconStateGloas): void {
92
- // Track pubkeys of new validators to keep their deposits pending
93
- const validatorPubkeys = new Set<string>();
94
-
95
93
  // Track pubkeys of new builders added when applying deposits
96
94
  const builderPubkeys = new Set<string>();
97
95
 
98
- const remainingPendingDeposits = ssz.electra.PendingDeposits.defaultViewDU();
96
+ const pendingDeposits = ssz.electra.PendingDeposits.defaultViewDU();
97
+ const pendingDepositsLookup = PendingDepositsLookup.buildEmpty();
98
+
99
99
  for (let i = 0; i < state.pendingDeposits.length; i++) {
100
100
  const deposit = state.pendingDeposits.getReadonly(i);
101
101
 
102
102
  const validatorIndex = state.epochCtx.getValidatorIndex(deposit.pubkey);
103
- const pubkeyHex = toHex(deposit.pubkey);
103
+ const pubkeyHex = toPubkeyHex(deposit.pubkey);
104
104
 
105
- // Deposits for existing validators stay in pending queue
106
- if (isValidatorKnown(state, validatorIndex) || validatorPubkeys.has(pubkeyHex)) {
107
- remainingPendingDeposits.push(deposit);
105
+ // Deposits for existing validators stay in the pending queue
106
+ if (isValidatorKnown(state, validatorIndex)) {
107
+ pendingDeposits.push(deposit);
108
+ pendingDepositsLookup.add(deposit, pubkeyHex);
108
109
  continue;
109
110
  }
110
111
 
111
- // If the pubkey is associated with a builder that was created in a previous iteration
112
- // or it is a builder deposit, try to apply the deposit to the new/existing builder
113
- const isExistingBuilder = builderPubkeys.has(pubkeyHex);
114
- const hasBuilderCredentials = isBuilderWithdrawalCredential(deposit.withdrawalCredentials);
115
- if (isExistingBuilder || hasBuilderCredentials) {
116
- const buildersLenBefore = state.builders.length;
117
- applyDepositForBuilder(
118
- state,
119
- deposit.pubkey,
120
- deposit.withdrawalCredentials,
121
- deposit.amount,
122
- deposit.signature,
123
- deposit.slot
124
- );
125
- // Track newly added builders for subsequent iterations
126
- if (!isExistingBuilder && state.builders.length > buildersLenBefore) {
127
- builderPubkeys.add(pubkeyHex);
112
+ // `applyDepositForBuilder` can mutate the state and add a builder to the registry, so
113
+ // the set of builder pubkeys must be recomputed each iteration. `builderPubkeys` stands
114
+ // in for the spec's `[b.pubkey for b in state.builders]`: `state.builders` starts empty
115
+ // at the fork, so every builder is one added in a previous iteration of this loop.
116
+ if (!builderPubkeys.has(pubkeyHex)) {
117
+ // Deposits for non-builders stay in the pending queue. If there is a valid pending
118
+ // deposit for a new validator with this pubkey, keep this deposit in the pending
119
+ // queue to be applied to that validator later.
120
+ if (!isBuilderWithdrawalCredential(deposit.withdrawalCredentials)) {
121
+ pendingDeposits.push(deposit);
122
+ pendingDepositsLookup.add(deposit, pubkeyHex);
123
+ continue;
124
+ }
125
+ if (pendingDepositsLookup.hasPendingValidator(state.config, pubkeyHex)) {
126
+ pendingDeposits.push(deposit);
127
+ pendingDepositsLookup.add(deposit, pubkeyHex);
128
+ continue;
128
129
  }
129
- continue;
130
130
  }
131
131
 
132
- // If there is a pending deposit for a new validator that has a valid signature, track the
133
- // pubkey so that subsequent builder deposits for the same pubkey stay in pending (applied to
134
- // the validator later) rather than creating a builder. Deposits with invalid signatures are
135
- // dropped here since they would fail in apply_pending_deposit anyway.
136
- if (
137
- isValidDepositSignature(
138
- state.config,
139
- deposit.pubkey,
140
- deposit.withdrawalCredentials,
141
- deposit.amount,
142
- deposit.signature
143
- )
144
- ) {
145
- validatorPubkeys.add(pubkeyHex);
146
- remainingPendingDeposits.push(deposit);
132
+ const buildersLenBefore = state.builders.length;
133
+ // TODO GLOAS: handle 20k 1ETH deposits on time
134
+ // there is a note in the spec https://github.com/ethereum/consensus-specs/pull/5227
135
+ applyDepositForBuilder(
136
+ state,
137
+ deposit.pubkey,
138
+ deposit.withdrawalCredentials,
139
+ deposit.amount,
140
+ deposit.signature,
141
+ deposit.slot
142
+ );
143
+ if (state.builders.length > buildersLenBefore) {
144
+ builderPubkeys.add(pubkeyHex);
147
145
  }
148
146
  }
149
147
 
150
- state.pendingDeposits = remainingPendingDeposits;
148
+ state.pendingDeposits = pendingDeposits;
151
149
  }
@@ -423,16 +423,27 @@ export class BeaconStateView implements IBeaconStateViewLatestFork {
423
423
  throw new Error(`PTC committees are not available for epoch=${epoch}`);
424
424
  }
425
425
  /**
426
- * Return the index of the validator in the PTC committee for the given slot.
427
- * return -1 if validator is not in the PTC committee for the given slot.
426
+ * Return all positions of the validator in the PTC committee for the given slot.
427
+ *
428
+ * `compute_ptc` samples by effective balance and may place the same validator at multiple
429
+ * positions, so a validator can have more than one index. Returns an empty array if the
430
+ * validator is not in the PTC for the given slot.
431
+ *
432
+ * Spec: gloas/fork-choice.md#new-on_payload_attestation_message
428
433
  */
429
- getIndexInPayloadTimelinessCommittee(validatorIndex: ValidatorIndex, slot: Slot): number {
434
+ getIndicesInPayloadTimelinessCommittee(validatorIndex: ValidatorIndex, slot: Slot): number[] {
430
435
  if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.gloas) {
431
436
  throw new Error("PTC committees are not supported before Gloas");
432
437
  }
433
438
 
434
439
  const ptcCommittee = (this.cachedState as CachedBeaconStateGloas).epochCtx.getPayloadTimelinessCommittee(slot);
435
- return ptcCommittee.indexOf(validatorIndex);
440
+ const indices: number[] = [];
441
+ for (let i = 0; i < ptcCommittee.length; i++) {
442
+ if (ptcCommittee[i] === validatorIndex) {
443
+ indices.push(i);
444
+ }
445
+ }
446
+ return indices;
436
447
  }
437
448
 
438
449
  // Shuffling and committees
@@ -487,6 +498,14 @@ export class BeaconStateView implements IBeaconStateViewLatestFork {
487
498
  return this.cachedState.epochCtx.getBeaconProposer(slot);
488
499
  }
489
500
 
501
+ getBeaconProposerOrNull(slot: Slot): ValidatorIndex | null {
502
+ try {
503
+ return this.cachedState.epochCtx.getBeaconProposer(slot);
504
+ } catch {
505
+ return null;
506
+ }
507
+ }
508
+
490
509
  computeAnchorCheckpoint(): {checkpoint: phase0.Checkpoint; blockHeader: phase0.BeaconBlockHeader} {
491
510
  return computeAnchorCheckpoint(this.config, this.cachedState);
492
511
  }
@@ -89,6 +89,7 @@ export interface IBeaconStateView {
89
89
  currentProposers: ValidatorIndex[];
90
90
  nextProposers: ValidatorIndex[];
91
91
  getBeaconProposer(slot: Slot): ValidatorIndex;
92
+ getBeaconProposerOrNull(slot: Slot): ValidatorIndex | null;
92
93
 
93
94
  // Validators and balances
94
95
  effectiveBalanceIncrements: EffectiveBalanceIncrements;
@@ -253,7 +254,7 @@ export interface IBeaconStateViewGloas extends IBeaconStateViewFulu {
253
254
  getBuilder(index: BuilderIndex): gloas.Builder;
254
255
  canBuilderCoverBid(builderIndex: BuilderIndex, bidAmount: number): boolean;
255
256
  getEpochPTCs(epoch: Epoch): Uint32Array[];
256
- getIndexInPayloadTimelinessCommittee(validatorIndex: ValidatorIndex, slot: Slot): number;
257
+ getIndicesInPayloadTimelinessCommittee(validatorIndex: ValidatorIndex, slot: Slot): number[];
257
258
  /**
258
259
  * Clone the state and apply parent execution payload effects.
259
260
  * Used during block production and prepareNextSlot so that withdrawals and
package/src/util/gloas.ts CHANGED
@@ -73,6 +73,34 @@ export function isActiveBuilder(builder: gloas.Builder, finalizedEpoch: Epoch):
73
73
  return builder.depositEpoch < finalizedEpoch && builder.withdrawableEpoch === FAR_FUTURE_EPOCH;
74
74
  }
75
75
 
76
+ /**
77
+ * Compute the gas limit that satisfies the EIP-1559 adjustment rule from `parentGasLimit`,
78
+ * clamping `targetGasLimit` into the allowed window of `±max(parentGasLimit / 1024, 1) - 1`.
79
+ *
80
+ * From https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md
81
+ */
82
+ export function getExpectedGasLimit(parentGasLimit: number, targetGasLimit: number): number {
83
+ const maxGasLimitDifference = Math.max(Math.floor(parentGasLimit / 1024), 1) - 1;
84
+
85
+ if (targetGasLimit > parentGasLimit) {
86
+ const gasDiff = targetGasLimit - parentGasLimit;
87
+ return parentGasLimit + Math.min(gasDiff, maxGasLimitDifference);
88
+ }
89
+
90
+ const gasDiff = parentGasLimit - targetGasLimit;
91
+ return parentGasLimit - Math.min(gasDiff, maxGasLimitDifference);
92
+ }
93
+
94
+ /**
95
+ * Check if `gasLimit` is compatible with `targetGasLimit` under the EIP-1559 transition rule
96
+ * from `parentGasLimit`. The bid must hit `targetGasLimit` when the target is within one
97
+ * adjustment step of the parent, otherwise it must hit the clamped boundary.
98
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.8/specs/gloas/builder.md#new-is_gas_limit_target_compatible
99
+ */
100
+ export function isGasLimitTargetCompatible(parentGasLimit: number, gasLimit: number, targetGasLimit: number): boolean {
101
+ return gasLimit === getExpectedGasLimit(parentGasLimit, targetGasLimit);
102
+ }
103
+
76
104
  /**
77
105
  * Get the total pending balance to withdraw for a builder (from withdrawals + payments).
78
106
  * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-get_pending_balance_to_withdraw_for_builder
package/src/util/index.ts CHANGED
@@ -18,6 +18,7 @@ export * from "./genesis.js";
18
18
  export * from "./gloas.js";
19
19
  export * from "./interop.js";
20
20
  export * from "./loadState/index.js";
21
+ export * from "./pendingDepositsLookup.js";
21
22
  export * from "./rootCache.js";
22
23
  export * from "./seed.js";
23
24
  export * from "./shuffling.js";
@@ -0,0 +1,105 @@
1
+ import {BeaconConfig} from "@lodestar/config";
2
+ import {PubkeyHex, electra} from "@lodestar/types";
3
+ import {toPubkeyHex} from "@lodestar/utils";
4
+ import {isValidDepositSignature} from "../block/processDeposit.js";
5
+ import {CachedBeaconStateGloas} from "../types.js";
6
+
7
+ type PendingDepositsValidation = {
8
+ hasValidSignature: boolean;
9
+ validatedCount: number;
10
+ };
11
+
12
+ /**
13
+ * Mutable lookup for the pending-deposit sequence used by builder-routing logic.
14
+ *
15
+ * This is to implement the spec's `is_pending_validator(pending_deposits, pubkey)` lazily:
16
+ * deposits are grouped by pubkey without verifying signatures, and BLS verification is
17
+ * deferred until a builder deposit needs to know whether the same pubkey already has a
18
+ * valid pending validator deposit.
19
+ *
20
+ * Call `add()` whenever a deposit is appended to the represented sequence. A cached `true`
21
+ * result short-circuits all subsequent checks for that pubkey; a cached `false` records
22
+ * how many deposits were already verified, so appending a new deposit only verifies the
23
+ * newly-appended tail rather than re-running BLS on previously-invalid entries.
24
+ */
25
+ export class PendingDepositsLookup {
26
+ private constructor(
27
+ private readonly depositsByPubkey: Map<PubkeyHex, electra.PendingDeposit[]>,
28
+ private readonly validationCache: Map<PubkeyHex, PendingDepositsValidation>
29
+ ) {}
30
+
31
+ /** Build an empty lookup for a sequence that will be populated incrementally. */
32
+ static buildEmpty(): PendingDepositsLookup {
33
+ return new PendingDepositsLookup(new Map(), new Map());
34
+ }
35
+
36
+ /**
37
+ * Build a pubkey -> pending-deposits lookup from `state.pendingDeposits`.
38
+ * No BLS work is done here; signature verification happens lazily in `hasPendingValidator`.
39
+ */
40
+ static build(state: CachedBeaconStateGloas): PendingDepositsLookup {
41
+ const lookup = PendingDepositsLookup.buildEmpty();
42
+ for (const pendingDeposit of state.pendingDeposits.getAllReadonly()) {
43
+ lookup.add(pendingDeposit);
44
+ }
45
+ return lookup;
46
+ }
47
+
48
+ /**
49
+ * Append a pending deposit to the represented sequence.
50
+ * Pass `pubkeyHex` if the caller has already computed it.
51
+ */
52
+ add(pendingDeposit: electra.PendingDeposit, pubkeyHex?: PubkeyHex): void {
53
+ const key = pubkeyHex ?? toPubkeyHex(pendingDeposit.pubkey);
54
+ const existing = this.depositsByPubkey.get(key);
55
+ if (existing) {
56
+ existing.push(pendingDeposit);
57
+ } else {
58
+ this.depositsByPubkey.set(key, [pendingDeposit]);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Returns true if any pending deposit for `pubkeyHex` has a valid BLS deposit signature.
64
+ * Memoizes the result in `validationCache` so repeated checks for the same pubkey
65
+ * within a block only verify deposits that have not already been checked.
66
+ */
67
+ hasPendingValidator(config: BeaconConfig, pubkeyHex: PubkeyHex): boolean {
68
+ const validation = this.validationCache.get(pubkeyHex);
69
+ if (validation?.hasValidSignature === true) {
70
+ return true;
71
+ }
72
+
73
+ const deposits = this.depositsByPubkey.get(pubkeyHex);
74
+ if (deposits === undefined) {
75
+ return false;
76
+ }
77
+
78
+ // hasValidSignature is false or undefined; resume from the last validatedCount so
79
+ // previously-checked invalid deposits are not re-verified.
80
+ const startIndex = validation?.validatedCount ?? 0;
81
+ if (startIndex === deposits.length) {
82
+ // Nothing new to check; the cached false result still holds.
83
+ return false;
84
+ }
85
+
86
+ for (let i = startIndex; i < deposits.length; i++) {
87
+ const deposit = deposits[i];
88
+ if (
89
+ isValidDepositSignature(
90
+ config,
91
+ deposit.pubkey,
92
+ deposit.withdrawalCredentials,
93
+ deposit.amount,
94
+ deposit.signature
95
+ )
96
+ ) {
97
+ this.validationCache.set(pubkeyHex, {hasValidSignature: true, validatedCount: i + 1});
98
+ return true;
99
+ }
100
+ }
101
+
102
+ this.validationCache.set(pubkeyHex, {hasValidSignature: false, validatedCount: deposits.length});
103
+ return false;
104
+ }
105
+ }
@@ -17,29 +17,31 @@ import {computeStartSlotAtEpoch} from "./epoch.js";
17
17
  import {EpochShuffling} from "./epochShuffling.js";
18
18
 
19
19
  /**
20
- * Returns the block root which decided the proposer shuffling for the current epoch. This root
21
- * can be used to key this proposer shuffling.
20
+ * Block root that decided the proposer shuffling for `proposalEpoch` (keys that shuffling).
21
+ * Computed for the requested epoch, not `state.epoch`, so it is correct when `state` is one
22
+ * epoch off the requested epoch (e.g. serving next-epoch duties from the current state).
22
23
  *
23
- * Returns `null` on the one-off scenario where the genesis block decides its own shuffling.
24
- * It should be set to the latest block applied to this `state` or the genesis block root.
24
+ * Returns `null` when the genesis block decides its own shuffling (caller falls back to the
25
+ * genesis block root).
25
26
  */
26
- export function proposerShufflingDecisionRoot(fork: ForkName, state: IBeaconStateView): Root | null {
27
- const decisionSlot = proposerShufflingDecisionSlot(fork, state);
27
+ export function proposerShufflingDecisionRoot(
28
+ fork: ForkName,
29
+ state: IBeaconStateView,
30
+ proposalEpoch: Epoch
31
+ ): Root | null {
32
+ const decisionSlot = proposerShufflingDecisionSlot(fork, proposalEpoch);
28
33
  if (state.slot === decisionSlot) {
29
34
  return null;
30
35
  }
31
36
  return state.getBlockRootAtSlot(decisionSlot);
32
37
  }
33
38
 
34
- /**
35
- * Returns the slot at which the proposer shuffling was decided. The block root at this slot
36
- * can be used to key the proposer shuffling for the current epoch.
37
- */
38
- function proposerShufflingDecisionSlot(fork: ForkName, state: IBeaconStateView): Slot {
39
- // After fulu, the decision slot is in previous epoch due to deterministic proposer lookahead
40
- const epoch = isForkPostFulu(fork) ? state.epoch - 1 : state.epoch;
41
- const startSlot = computeStartSlotAtEpoch(epoch);
42
- return Math.max(startSlot - 1, 0);
39
+ /** Slot whose block root keys the proposer shuffling for `proposalEpoch`. */
40
+ function proposerShufflingDecisionSlot(fork: ForkName, proposalEpoch: Epoch): Slot {
41
+ // Post-Fulu the shuffling is decided one epoch earlier (deterministic proposer lookahead,
42
+ // MIN_SEED_LOOKAHEAD = 1); pre-Fulu it is the last block before `proposalEpoch`.
43
+ const decisionEpoch = isForkPostFulu(fork) ? proposalEpoch - 1 : proposalEpoch;
44
+ return Math.max(computeStartSlotAtEpoch(decisionEpoch) - 1, 0);
43
45
  }
44
46
 
45
47
  /**