@lodestar/fork-choice 1.44.0-dev.552cdce8d0 → 1.44.0-dev.6c5c48ad59
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.
- package/lib/forkChoice/forkChoice.d.ts +14 -4
- package/lib/forkChoice/forkChoice.d.ts.map +1 -1
- package/lib/forkChoice/forkChoice.js +52 -30
- package/lib/forkChoice/forkChoice.js.map +1 -1
- package/lib/forkChoice/interface.d.ts +17 -8
- package/lib/forkChoice/interface.d.ts.map +1 -1
- package/lib/forkChoice/interface.js.map +1 -1
- package/lib/protoArray/interface.d.ts +1 -0
- package/lib/protoArray/interface.d.ts.map +1 -1
- package/lib/protoArray/protoArray.d.ts +57 -21
- package/lib/protoArray/protoArray.d.ts.map +1 -1
- package/lib/protoArray/protoArray.js +151 -39
- package/lib/protoArray/protoArray.js.map +1 -1
- package/package.json +7 -7
- package/src/forkChoice/forkChoice.ts +70 -31
- package/src/forkChoice/interface.ts +33 -7
- package/src/protoArray/interface.ts +8 -0
- package/src/protoArray/protoArray.ts +161 -41
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"bugs": {
|
|
12
12
|
"url": "https://github.com/ChainSafe/lodestar/issues"
|
|
13
13
|
},
|
|
14
|
-
"version": "1.44.0-dev.
|
|
14
|
+
"version": "1.44.0-dev.6c5c48ad59",
|
|
15
15
|
"type": "module",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@chainsafe/ssz": "^1.4.0",
|
|
43
|
-
"@lodestar/config": "^1.44.0-dev.
|
|
44
|
-
"@lodestar/params": "^1.44.0-dev.
|
|
45
|
-
"@lodestar/state-transition": "^1.44.0-dev.
|
|
46
|
-
"@lodestar/types": "^1.44.0-dev.
|
|
47
|
-
"@lodestar/utils": "^1.44.0-dev.
|
|
43
|
+
"@lodestar/config": "^1.44.0-dev.6c5c48ad59",
|
|
44
|
+
"@lodestar/params": "^1.44.0-dev.6c5c48ad59",
|
|
45
|
+
"@lodestar/state-transition": "^1.44.0-dev.6c5c48ad59",
|
|
46
|
+
"@lodestar/types": "^1.44.0-dev.6c5c48ad59",
|
|
47
|
+
"@lodestar/utils": "^1.44.0-dev.6c5c48ad59"
|
|
48
48
|
},
|
|
49
49
|
"keywords": [
|
|
50
50
|
"ethereum",
|
|
@@ -52,5 +52,5 @@
|
|
|
52
52
|
"beacon",
|
|
53
53
|
"blockchain"
|
|
54
54
|
],
|
|
55
|
-
"gitHead": "
|
|
55
|
+
"gitHead": "13f2b4ed7034c44a3d6a924411256472865d6fc8"
|
|
56
56
|
}
|
|
@@ -310,6 +310,10 @@ export class ForkChoice implements IForkChoice {
|
|
|
310
310
|
return this.proposerBoostRoot ?? HEX_ZERO_HASH;
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
getPreviousProposerBoostRoot(): RootHex {
|
|
314
|
+
return this.protoArray.getPreviousProposerBoostRoot();
|
|
315
|
+
}
|
|
316
|
+
|
|
313
317
|
/**
|
|
314
318
|
* Decides whether to extend an available payload from the previous slot,
|
|
315
319
|
* corresponding to the beacon block `blockRoot`.
|
|
@@ -318,6 +322,11 @@ export class ForkChoice implements IForkChoice {
|
|
|
318
322
|
return this.protoArray.shouldExtendPayload(blockRoot, this.proposerBoostRoot);
|
|
319
323
|
}
|
|
320
324
|
|
|
325
|
+
/** Spec: should_build_on_full(store, head) */
|
|
326
|
+
shouldBuildOnFull(head: ProtoBlock, slot: Slot): boolean {
|
|
327
|
+
return this.protoArray.shouldBuildOnFull(head, slot);
|
|
328
|
+
}
|
|
329
|
+
|
|
321
330
|
/**
|
|
322
331
|
* To predict the proposer head of the next slot. That is, to predict if proposer-boost-reorg could happen.
|
|
323
332
|
* Reason why we can't be certain is because information of the head block is not fully available yet
|
|
@@ -596,7 +605,11 @@ export class ForkChoice implements IForkChoice {
|
|
|
596
605
|
blockDelaySec: number,
|
|
597
606
|
currentSlot: Slot,
|
|
598
607
|
executionStatus: BlockExecutionStatus,
|
|
599
|
-
dataAvailabilityStatus: DataAvailabilityStatus
|
|
608
|
+
dataAvailabilityStatus: DataAvailabilityStatus,
|
|
609
|
+
// The expected proposer index on the canonical chain we are following.
|
|
610
|
+
// Calculated by our head state. We use it as part of the proposer
|
|
611
|
+
// boost decision making. No boost will be set if this is null.
|
|
612
|
+
expectedProposerIndex: ValidatorIndex | null
|
|
600
613
|
): ProtoBlock {
|
|
601
614
|
const {parentRoot, slot} = block;
|
|
602
615
|
const parentRootHex = toRootHex(parentRoot);
|
|
@@ -669,7 +682,9 @@ export class ForkChoice implements IForkChoice {
|
|
|
669
682
|
this.opts?.proposerBoost &&
|
|
670
683
|
isTimely &&
|
|
671
684
|
// only boost the first block we see
|
|
672
|
-
this.proposerBoostRoot === null
|
|
685
|
+
this.proposerBoostRoot === null &&
|
|
686
|
+
expectedProposerIndex !== null &&
|
|
687
|
+
block.proposerIndex === expectedProposerIndex
|
|
673
688
|
) {
|
|
674
689
|
this.proposerBoostRoot = blockRootHex;
|
|
675
690
|
}
|
|
@@ -764,33 +779,32 @@ export class ForkChoice implements IForkChoice {
|
|
|
764
779
|
unrealizedFinalizedRoot: unrealizedFinalizedCheckpoint.rootHex,
|
|
765
780
|
|
|
766
781
|
...(isGloasBeaconBlock(block)
|
|
767
|
-
? {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
}
|
|
782
|
+
? (() => {
|
|
783
|
+
// post-gloas, we don't know payload hash until we import execution payload. Set to
|
|
784
|
+
// parent payload hash for now, along with the gas limit/number of that parent payload
|
|
785
|
+
// (which is what bids built on top of this block will reference until a payload arrives).
|
|
786
|
+
// we also use parent hash to pass to EL via fcu
|
|
787
|
+
// see https://github.com/ethereum/consensus-specs/pull/5197
|
|
788
|
+
const parentBlockHashFromBid = toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash);
|
|
789
|
+
|
|
790
|
+
// Inherit parent payload's (number, gasLimit) for the PENDING/EMPTY variants.
|
|
791
|
+
// `parentBlock` is already the variant matching `parentBlockHashFromBid` —
|
|
792
|
+
// `getParent` (called above) resolves Gloas parents via
|
|
793
|
+
// `getBlockHexAndBlockHash(parentRoot, parentBlockHash)`, and pre-Gloas parents
|
|
794
|
+
// have a single variant. Pre-merge parents have null payload hash and zero values.
|
|
795
|
+
const parentMeta: {number: number; gasLimit: number} =
|
|
796
|
+
parentBlock.executionPayloadBlockHash === null
|
|
797
|
+
? {number: 0, gasLimit: 0}
|
|
798
|
+
: {number: parentBlock.executionPayloadNumber, gasLimit: parentBlock.executionPayloadGasLimit};
|
|
799
|
+
|
|
800
|
+
return {
|
|
801
|
+
executionPayloadBlockHash: parentBlockHashFromBid,
|
|
802
|
+
executionPayloadNumber: parentMeta.number,
|
|
803
|
+
executionPayloadGasLimit: parentMeta.gasLimit,
|
|
804
|
+
executionStatus: this.getPostMergeExecStatus(executionStatus),
|
|
805
|
+
dataAvailabilityStatus,
|
|
806
|
+
};
|
|
807
|
+
})()
|
|
794
808
|
: isExecutionBlockBodyType(block.body) &&
|
|
795
809
|
isStatePostBellatrix(state) &&
|
|
796
810
|
state.isExecutionStateType &&
|
|
@@ -798,6 +812,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
798
812
|
? {
|
|
799
813
|
executionPayloadBlockHash: toRootHex(block.body.executionPayload.blockHash),
|
|
800
814
|
executionPayloadNumber: block.body.executionPayload.blockNumber,
|
|
815
|
+
executionPayloadGasLimit: block.body.executionPayload.gasLimit,
|
|
801
816
|
executionStatus: this.getPostMergeExecStatus(executionStatus),
|
|
802
817
|
dataAvailabilityStatus,
|
|
803
818
|
}
|
|
@@ -934,8 +949,14 @@ export class ForkChoice implements IForkChoice {
|
|
|
934
949
|
* Updates the PTC votes for multiple validators attesting to a block
|
|
935
950
|
* Spec: gloas/fork-choice.md#new-on_payload_attestation_message
|
|
936
951
|
*/
|
|
937
|
-
notifyPtcMessages(
|
|
938
|
-
|
|
952
|
+
notifyPtcMessages(
|
|
953
|
+
blockRoot: RootHex,
|
|
954
|
+
slot: Slot,
|
|
955
|
+
ptcIndices: number[],
|
|
956
|
+
payloadPresent: boolean,
|
|
957
|
+
blobDataAvailable: boolean
|
|
958
|
+
): void {
|
|
959
|
+
this.protoArray.notifyPtcMessages(blockRoot, slot, ptcIndices, payloadPresent, blobDataAvailable);
|
|
939
960
|
}
|
|
940
961
|
|
|
941
962
|
/**
|
|
@@ -947,6 +968,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
947
968
|
blockRoot: RootHex,
|
|
948
969
|
executionPayloadBlockHash: RootHex,
|
|
949
970
|
executionPayloadNumber: number,
|
|
971
|
+
executionPayloadGasLimit: number,
|
|
950
972
|
executionStatus: PayloadExecutionStatus,
|
|
951
973
|
dataAvailabilityStatus: DataAvailabilityStatus
|
|
952
974
|
): void {
|
|
@@ -955,6 +977,7 @@ export class ForkChoice implements IForkChoice {
|
|
|
955
977
|
this.fcStore.currentSlot,
|
|
956
978
|
executionPayloadBlockHash,
|
|
957
979
|
executionPayloadNumber,
|
|
980
|
+
executionPayloadGasLimit,
|
|
958
981
|
this.proposerBoostRoot,
|
|
959
982
|
executionStatus,
|
|
960
983
|
dataAvailabilityStatus
|
|
@@ -1049,6 +1072,22 @@ export class ForkChoice implements IForkChoice {
|
|
|
1049
1072
|
return votes.toBoolArray().map((v) => v ?? null);
|
|
1050
1073
|
}
|
|
1051
1074
|
|
|
1075
|
+
getPTCVoteCounts(blockRootHex: RootHex): {
|
|
1076
|
+
attesterCount: number;
|
|
1077
|
+
payloadPresentCount: number;
|
|
1078
|
+
dataAvailableCount: number;
|
|
1079
|
+
} | null {
|
|
1080
|
+
return this.protoArray.getPTCVoteCounts(blockRootHex);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
getUnrealizedJustifiedCheckpoint(): CheckpointWithHex {
|
|
1084
|
+
return this.fcStore.unrealizedJustified.checkpoint;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
getUnrealizedFinalizedCheckpoint(): CheckpointWithHex {
|
|
1088
|
+
return this.fcStore.unrealizedFinalizedCheckpoint;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1052
1091
|
/**
|
|
1053
1092
|
* Returns a MUTABLE `ProtoBlock` if the block is known **and** a descendant of the finalized root.
|
|
1054
1093
|
*/
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import {DataAvailabilityStatus, EffectiveBalanceIncrements, IBeaconStateView} from "@lodestar/state-transition";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
AttesterSlashing,
|
|
4
|
+
BeaconBlock,
|
|
5
|
+
Epoch,
|
|
6
|
+
IndexedAttestation,
|
|
7
|
+
Root,
|
|
8
|
+
RootHex,
|
|
9
|
+
Slot,
|
|
10
|
+
ValidatorIndex,
|
|
11
|
+
} from "@lodestar/types";
|
|
3
12
|
import {
|
|
4
13
|
BlockExecutionStatus,
|
|
5
14
|
LVHExecResponse,
|
|
@@ -123,6 +132,10 @@ export interface IForkChoice {
|
|
|
123
132
|
getAllNodes(): ProtoNode[];
|
|
124
133
|
getFinalizedCheckpoint(): CheckpointWithHex;
|
|
125
134
|
getJustifiedCheckpoint(): CheckpointWithHex;
|
|
135
|
+
getUnrealizedJustifiedCheckpoint(): CheckpointWithHex;
|
|
136
|
+
getUnrealizedFinalizedCheckpoint(): CheckpointWithHex;
|
|
137
|
+
getProposerBoostRoot(): RootHex;
|
|
138
|
+
getPreviousProposerBoostRoot(): RootHex;
|
|
126
139
|
/**
|
|
127
140
|
* Add `block` to the fork choice DAG.
|
|
128
141
|
*
|
|
@@ -145,7 +158,8 @@ export interface IForkChoice {
|
|
|
145
158
|
blockDelaySec: number,
|
|
146
159
|
currentSlot: Slot,
|
|
147
160
|
executionStatus: BlockExecutionStatus,
|
|
148
|
-
dataAvailabilityStatus: DataAvailabilityStatus
|
|
161
|
+
dataAvailabilityStatus: DataAvailabilityStatus,
|
|
162
|
+
expectedProposerIndex: ValidatorIndex | null
|
|
149
163
|
): ProtoBlock;
|
|
150
164
|
/**
|
|
151
165
|
* Register `attestation` with the fork choice DAG so that it may influence future calls to `getHead`.
|
|
@@ -181,12 +195,14 @@ export interface IForkChoice {
|
|
|
181
195
|
* ## Specification
|
|
182
196
|
*
|
|
183
197
|
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.0/specs/gloas/fork-choice.md#new-notify_ptc_messages
|
|
184
|
-
*
|
|
185
|
-
* @param blockRoot - The beacon block root being attested
|
|
186
|
-
* @param ptcIndices - Array of PTC committee indices that voted
|
|
187
|
-
* @param payloadPresent - Whether validators attest the payload is present
|
|
188
198
|
*/
|
|
189
|
-
notifyPtcMessages(
|
|
199
|
+
notifyPtcMessages(
|
|
200
|
+
blockRoot: RootHex,
|
|
201
|
+
slot: Slot,
|
|
202
|
+
ptcIndices: number[],
|
|
203
|
+
payloadPresent: boolean,
|
|
204
|
+
blobDataAvailable: boolean
|
|
205
|
+
): void;
|
|
190
206
|
/**
|
|
191
207
|
* Notify fork choice that an execution payload has arrived (Gloas fork)
|
|
192
208
|
* Creates the FULL variant of a Gloas block when the payload becomes available
|
|
@@ -198,11 +214,13 @@ export interface IForkChoice {
|
|
|
198
214
|
* @param blockRoot - The beacon block root for which the payload arrived
|
|
199
215
|
* @param executionPayloadBlockHash - The block hash of the execution payload
|
|
200
216
|
* @param executionPayloadNumber - The block number of the execution payload
|
|
217
|
+
* @param executionPayloadGasLimit - The gas limit of the execution payload
|
|
201
218
|
*/
|
|
202
219
|
onExecutionPayload(
|
|
203
220
|
blockRoot: RootHex,
|
|
204
221
|
executionPayloadBlockHash: RootHex,
|
|
205
222
|
executionPayloadNumber: number,
|
|
223
|
+
executionPayloadGasLimit: number,
|
|
206
224
|
executionStatus: PayloadExecutionStatus,
|
|
207
225
|
dataAvailabilityStatus: DataAvailabilityStatus
|
|
208
226
|
): void;
|
|
@@ -233,6 +251,12 @@ export interface IForkChoice {
|
|
|
233
251
|
hasPayloadHexUnsafe(blockRoot: RootHex): boolean;
|
|
234
252
|
getSlotsPresent(windowStart: number): number;
|
|
235
253
|
getPTCVotes(blockRootHex: RootHex): (boolean | null)[] | null;
|
|
254
|
+
/** Raw PTC vote tallies for the debug fork choice endpoint; `null` for pre-Gloas roots. */
|
|
255
|
+
getPTCVoteCounts(blockRootHex: RootHex): {
|
|
256
|
+
attesterCount: number;
|
|
257
|
+
payloadPresentCount: number;
|
|
258
|
+
dataAvailableCount: number;
|
|
259
|
+
} | null;
|
|
236
260
|
/**
|
|
237
261
|
* Returns a `ProtoBlock` if the block is known **and** a descendant of the finalized root.
|
|
238
262
|
*/
|
|
@@ -242,6 +266,8 @@ export interface IForkChoice {
|
|
|
242
266
|
getBlockHexDefaultStatus(blockRoot: RootHex): ProtoBlock | null;
|
|
243
267
|
getBlockHexAndBlockHash(blockRoot: RootHex, blockHash: RootHex): ProtoBlock | null;
|
|
244
268
|
shouldExtendPayload(blockRoot: RootHex): boolean;
|
|
269
|
+
/** Spec: should_build_on_full(store, head) */
|
|
270
|
+
shouldBuildOnFull(head: ProtoBlock, slot: Slot): boolean;
|
|
245
271
|
getFinalizedBlock(): ProtoBlock;
|
|
246
272
|
getJustifiedBlock(): ProtoBlock;
|
|
247
273
|
getFinalizedCheckpointSlot(): Slot;
|
|
@@ -87,6 +87,14 @@ export type BlockExtraMeta =
|
|
|
87
87
|
// - payload block hash for FULL variant
|
|
88
88
|
executionPayloadBlockHash: RootHex;
|
|
89
89
|
executionPayloadNumber: UintNum64;
|
|
90
|
+
// Gas limit of the executed payload identified by executionPayloadBlockHash. Set on
|
|
91
|
+
// pre-Gloas blocks (from block.body.executionPayload.gasLimit) and on Gloas variants:
|
|
92
|
+
// - PENDING/EMPTY: inherited from the parent payload that the bid commits to extend
|
|
93
|
+
// (matches executionPayloadBlockHash, which also points to that parent payload)
|
|
94
|
+
// - FULL: the actual delivered payload's gasLimit (set in onExecutionPayload)
|
|
95
|
+
// Consumers (e.g. Gloas bid gas-limit validation) can read this without re-deriving from
|
|
96
|
+
// state.
|
|
97
|
+
executionPayloadGasLimit: UintNum64;
|
|
90
98
|
executionStatus: Exclude<ExecutionStatus, ExecutionStatus.PreMerge>;
|
|
91
99
|
dataAvailabilityStatus: DataAvailabilityStatus;
|
|
92
100
|
}
|
|
@@ -21,6 +21,29 @@ import {
|
|
|
21
21
|
* Spec: gloas/fork-choice.md (PAYLOAD_TIMELY_THRESHOLD = PTC_SIZE // 2)
|
|
22
22
|
*/
|
|
23
23
|
const PAYLOAD_TIMELY_THRESHOLD = Math.floor(PTC_SIZE / 2);
|
|
24
|
+
/**
|
|
25
|
+
* Threshold for blob data availability via PTC vote
|
|
26
|
+
* Spec: gloas/fork-choice.md (DATA_AVAILABILITY_TIMELY_THRESHOLD = PTC_SIZE // 2)
|
|
27
|
+
*/
|
|
28
|
+
const DATA_AVAILABILITY_TIMELY_THRESHOLD = Math.floor(PTC_SIZE / 2);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* popcount(attended AND NOT yes) — explicit False-vote count.
|
|
32
|
+
* Excludes PTC members who didn't attest (the None state).
|
|
33
|
+
*/
|
|
34
|
+
export function countNoVotes(attended: BitArray, yes: BitArray): number {
|
|
35
|
+
const a = attended.uint8Array;
|
|
36
|
+
const y = yes.uint8Array;
|
|
37
|
+
let count = 0;
|
|
38
|
+
for (let i = 0; i < a.length; i++) {
|
|
39
|
+
let byte = a[i] & ~y[i] & 0xff;
|
|
40
|
+
while (byte) {
|
|
41
|
+
byte &= byte - 1;
|
|
42
|
+
count++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return count;
|
|
46
|
+
}
|
|
24
47
|
|
|
25
48
|
export const DEFAULT_PRUNE_THRESHOLD = 0;
|
|
26
49
|
type ProposerBoost = {root: RootHex; score: number};
|
|
@@ -64,12 +87,25 @@ export class ProtoArray {
|
|
|
64
87
|
/**
|
|
65
88
|
* PTC (Payload Timeliness Committee) votes per block as bitvectors
|
|
66
89
|
* Maps block root to BitArray of PTC_SIZE bits (512 mainnet, 2 minimal)
|
|
67
|
-
* Spec: gloas/fork-choice.md#modified-store (
|
|
90
|
+
* Spec: gloas/fork-choice.md#modified-store (payload_timeliness_vote)
|
|
68
91
|
*
|
|
69
|
-
* Bit i
|
|
70
|
-
* Used by is_payload_timely() to determine if payload is timely
|
|
92
|
+
* Bit i = PTC member i voted payloadPresent=true (timeliness YES vote)
|
|
71
93
|
*/
|
|
72
|
-
private
|
|
94
|
+
private payloadTimelinessVotes = new Map<RootHex, BitArray>();
|
|
95
|
+
/**
|
|
96
|
+
* Blob data availability votes per block.
|
|
97
|
+
* Spec: gloas/fork-choice.md#modified-store (payload_data_availability_vote)
|
|
98
|
+
*
|
|
99
|
+
* Bit i = PTC member i voted blobDataAvailable=true (DA YES vote)
|
|
100
|
+
*/
|
|
101
|
+
private payloadDataAvailabilityVotes = new Map<RootHex, BitArray>();
|
|
102
|
+
/**
|
|
103
|
+
* Tracks which PTC members have attested at all (any payload_status).
|
|
104
|
+
* Without this, we cannot tell "didn't vote" (None) from "voted false" —
|
|
105
|
+
* a distinction required by payload_timeliness/payload_data_availability
|
|
106
|
+
* when called with the negative parameter value.
|
|
107
|
+
*/
|
|
108
|
+
private ptcAttested = new Map<RootHex, BitArray>();
|
|
73
109
|
|
|
74
110
|
constructor({
|
|
75
111
|
pruneThreshold,
|
|
@@ -514,9 +550,11 @@ export class ProtoArray {
|
|
|
514
550
|
// Update bestChild for PENDING → EMPTY edge
|
|
515
551
|
this.maybeUpdateBestChildAndDescendant(pendingIndex, emptyIndex, currentSlot, proposerBoostRoot);
|
|
516
552
|
|
|
517
|
-
// Initialize PTC
|
|
518
|
-
// Spec: gloas/fork-choice.md#modified-on_block
|
|
519
|
-
this.
|
|
553
|
+
// Initialize PTC vote bitvectors for this block.
|
|
554
|
+
// Spec: gloas/fork-choice.md#modified-on_block
|
|
555
|
+
this.payloadTimelinessVotes.set(block.blockRoot, BitArray.fromBitLen(PTC_SIZE));
|
|
556
|
+
this.ptcAttested.set(block.blockRoot, BitArray.fromBitLen(PTC_SIZE));
|
|
557
|
+
this.payloadDataAvailabilityVotes.set(block.blockRoot, BitArray.fromBitLen(PTC_SIZE));
|
|
520
558
|
} else {
|
|
521
559
|
// Pre-Gloas: Only create FULL node (payload embedded in block)
|
|
522
560
|
const node: ProtoNode = {
|
|
@@ -558,6 +596,7 @@ export class ProtoArray {
|
|
|
558
596
|
currentSlot: Slot,
|
|
559
597
|
executionPayloadBlockHash: RootHex,
|
|
560
598
|
executionPayloadNumber: number,
|
|
599
|
+
executionPayloadGasLimit: number,
|
|
561
600
|
proposerBoostRoot: RootHex | null,
|
|
562
601
|
executionStatus: PayloadExecutionStatus,
|
|
563
602
|
dataAvailabilityStatus: DataAvailabilityStatus
|
|
@@ -613,6 +652,7 @@ export class ProtoArray {
|
|
|
613
652
|
executionStatus,
|
|
614
653
|
executionPayloadBlockHash,
|
|
615
654
|
executionPayloadNumber,
|
|
655
|
+
executionPayloadGasLimit,
|
|
616
656
|
dataAvailabilityStatus,
|
|
617
657
|
};
|
|
618
658
|
|
|
@@ -634,30 +674,42 @@ export class ProtoArray {
|
|
|
634
674
|
|
|
635
675
|
/**
|
|
636
676
|
* Update PTC votes for multiple validators attesting to a block
|
|
637
|
-
* Spec: gloas/fork-choice.md#new-
|
|
638
|
-
*
|
|
639
|
-
* @param blockRoot - The beacon block root being attested
|
|
640
|
-
* @param ptcIndices - Array of PTC committee indices that voted (0..PTC_SIZE-1)
|
|
641
|
-
* @param payloadPresent - Whether the validators attest the payload is present
|
|
677
|
+
* Spec: gloas/fork-choice.md#new-notify_ptc_messages
|
|
642
678
|
*/
|
|
643
|
-
notifyPtcMessages(
|
|
644
|
-
|
|
645
|
-
|
|
679
|
+
notifyPtcMessages(
|
|
680
|
+
blockRoot: RootHex,
|
|
681
|
+
slot: Slot,
|
|
682
|
+
ptcIndices: number[],
|
|
683
|
+
payloadPresent: boolean,
|
|
684
|
+
blobDataAvailable: boolean
|
|
685
|
+
): void {
|
|
686
|
+
const votes = this.payloadTimelinessVotes.get(blockRoot);
|
|
687
|
+
const attended = this.ptcAttested.get(blockRoot);
|
|
688
|
+
const daVotes = this.payloadDataAvailabilityVotes.get(blockRoot);
|
|
689
|
+
if (votes === undefined || attended === undefined || daVotes === undefined) {
|
|
646
690
|
// Block not found or not a Gloas block, ignore
|
|
647
691
|
return;
|
|
648
692
|
}
|
|
649
693
|
|
|
694
|
+
// PTC votes can only change the vote for their assigned beacon block, return early otherwise
|
|
695
|
+
const nodeIndex = this.getDefaultNodeIndex(blockRoot);
|
|
696
|
+
const node = nodeIndex !== undefined ? this.getNodeByIndex(nodeIndex) : undefined;
|
|
697
|
+
if (node === undefined || node.slot !== slot) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
650
701
|
for (const ptcIndex of ptcIndices) {
|
|
651
702
|
if (ptcIndex < 0 || ptcIndex >= PTC_SIZE) {
|
|
652
703
|
throw new Error(`Invalid PTC index: ${ptcIndex}, must be 0..${PTC_SIZE - 1}`);
|
|
653
704
|
}
|
|
654
|
-
|
|
655
705
|
votes.set(ptcIndex, payloadPresent);
|
|
706
|
+
daVotes.set(ptcIndex, blobDataAvailable);
|
|
707
|
+
attended.set(ptcIndex, true);
|
|
656
708
|
}
|
|
657
709
|
}
|
|
658
710
|
|
|
659
711
|
getPTCVotes(blockRootHex: RootHex): BitArray | null {
|
|
660
|
-
const votes = this.
|
|
712
|
+
const votes = this.payloadTimelinessVotes.get(blockRootHex);
|
|
661
713
|
if (votes === undefined) {
|
|
662
714
|
// Block not found or not a Gloas block
|
|
663
715
|
return null;
|
|
@@ -667,31 +719,97 @@ export class ProtoArray {
|
|
|
667
719
|
}
|
|
668
720
|
|
|
669
721
|
/**
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
* Returns true if:
|
|
674
|
-
* 1. Block has PTC votes tracked
|
|
675
|
-
* 2. Payload is locally available (FULL variant exists in proto array)
|
|
676
|
-
* 3. More than PAYLOAD_TIMELY_THRESHOLD (>50% of PTC) members voted payload_present=true
|
|
677
|
-
*
|
|
678
|
-
* @param blockRoot - The beacon block root to check
|
|
722
|
+
* Raw PTC vote tallies for a block root, for the debug fork choice endpoint.
|
|
723
|
+
* Returns `null` for pre-Gloas (or pruned) roots, which have no vote maps.
|
|
679
724
|
*/
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
725
|
+
getPTCVoteCounts(blockRootHex: RootHex): {
|
|
726
|
+
attesterCount: number;
|
|
727
|
+
payloadPresentCount: number;
|
|
728
|
+
dataAvailableCount: number;
|
|
729
|
+
} | null {
|
|
730
|
+
const attended = this.ptcAttested.get(blockRootHex);
|
|
731
|
+
const timelinessVotes = this.payloadTimelinessVotes.get(blockRootHex);
|
|
732
|
+
const daVotes = this.payloadDataAvailabilityVotes.get(blockRootHex);
|
|
733
|
+
// The three maps share a lifecycle (set together in onBlock, deleted together on prune)
|
|
734
|
+
if (attended === undefined || timelinessVotes === undefined || daVotes === undefined) {
|
|
735
|
+
return null;
|
|
685
736
|
}
|
|
737
|
+
return {
|
|
738
|
+
attesterCount: bitCount(attended.uint8Array),
|
|
739
|
+
payloadPresentCount: bitCount(timelinessVotes.uint8Array),
|
|
740
|
+
dataAvailableCount: bitCount(daVotes.uint8Array),
|
|
741
|
+
};
|
|
742
|
+
}
|
|
686
743
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
744
|
+
getPreviousProposerBoostRoot(): RootHex {
|
|
745
|
+
return this.previousProposerBoost?.root ?? HEX_ZERO_HASH;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Spec: payload_timeliness(store, root, timely=True)
|
|
750
|
+
*/
|
|
751
|
+
isPayloadTimely(blockRoot: RootHex): boolean {
|
|
752
|
+
const votes = this.payloadTimelinessVotes.get(blockRoot);
|
|
753
|
+
if (votes === undefined) return false;
|
|
754
|
+
if (!this.hasPayload(blockRoot)) return false;
|
|
755
|
+
return bitCount(votes.uint8Array) > PAYLOAD_TIMELY_THRESHOLD;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Spec: payload_timeliness(store, root, timely=False)
|
|
760
|
+
*/
|
|
761
|
+
isPayloadNotTimely(blockRoot: RootHex): boolean {
|
|
762
|
+
const votes = this.payloadTimelinessVotes.get(blockRoot);
|
|
763
|
+
const attended = this.ptcAttested.get(blockRoot);
|
|
764
|
+
if (votes === undefined || attended === undefined) return false;
|
|
765
|
+
// Spec: not verified locally → returns `not False = True`
|
|
766
|
+
if (!this.hasPayload(blockRoot)) return true;
|
|
767
|
+
return countNoVotes(attended, votes) > PAYLOAD_TIMELY_THRESHOLD;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Spec: payload_data_availability(store, root, available=True)
|
|
772
|
+
*/
|
|
773
|
+
isPayloadDataAvailable(blockRoot: RootHex): boolean {
|
|
774
|
+
const daVotes = this.payloadDataAvailabilityVotes.get(blockRoot);
|
|
775
|
+
if (daVotes === undefined) return false;
|
|
776
|
+
if (!this.hasPayload(blockRoot)) return false;
|
|
777
|
+
return bitCount(daVotes.uint8Array) > DATA_AVAILABILITY_TIMELY_THRESHOLD;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Spec: payload_data_availability(store, root, available=False)
|
|
782
|
+
*/
|
|
783
|
+
isPayloadDataNotAvailable(blockRoot: RootHex): boolean {
|
|
784
|
+
const daVotes = this.payloadDataAvailabilityVotes.get(blockRoot);
|
|
785
|
+
const attended = this.ptcAttested.get(blockRoot);
|
|
786
|
+
if (daVotes === undefined || attended === undefined) return false;
|
|
787
|
+
// Spec: not verified locally → returns `not False = True`
|
|
788
|
+
if (!this.hasPayload(blockRoot)) return true;
|
|
789
|
+
return countNoVotes(attended, daVotes) > DATA_AVAILABILITY_TIMELY_THRESHOLD;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Spec: should_build_on_full(store, head)
|
|
794
|
+
*
|
|
795
|
+
* The proposer is forced to build on the EMPTY variant (effectively reorging)
|
|
796
|
+
* when the PTC majority voted that the blob data is not available or that the
|
|
797
|
+
* payload was not timely.
|
|
798
|
+
*/
|
|
799
|
+
shouldBuildOnFull(head: ProtoBlock, slot: Slot): boolean {
|
|
800
|
+
if (head.payloadStatus === PayloadStatus.PENDING) {
|
|
801
|
+
throw new Error("shouldBuildOnFull called with PENDING head");
|
|
690
802
|
}
|
|
803
|
+
if (head.payloadStatus === PayloadStatus.EMPTY) return false;
|
|
804
|
+
|
|
805
|
+
// The PTC data availability and timeliness views are only consulted for a head from the
|
|
806
|
+
// previous slot. For an earlier head the empty/full variant has already been resolved by
|
|
807
|
+
// weight in getHead.
|
|
808
|
+
if (head.slot + 1 !== slot) return true;
|
|
809
|
+
|
|
810
|
+
if (this.isPayloadDataNotAvailable(head.blockRoot)) return false;
|
|
691
811
|
|
|
692
|
-
|
|
693
|
-
const yesVotes = bitCount(votes.uint8Array);
|
|
694
|
-
return yesVotes > PAYLOAD_TIMELY_THRESHOLD;
|
|
812
|
+
return !this.isPayloadNotTimely(head.blockRoot);
|
|
695
813
|
}
|
|
696
814
|
|
|
697
815
|
/**
|
|
@@ -709,7 +827,7 @@ export class ProtoArray {
|
|
|
709
827
|
* Spec: gloas/fork-choice.md#new-should_extend_payload
|
|
710
828
|
*
|
|
711
829
|
* Returns true if payload is verified (FULL variant exists) AND:
|
|
712
|
-
* 1. Payload is timely, OR
|
|
830
|
+
* 1. Payload is timely AND data is available, OR
|
|
713
831
|
* 2. No proposer boost root (empty/zero hash), OR
|
|
714
832
|
* 3. Proposer boost root's parent is not this block, OR
|
|
715
833
|
* 4. Proposer boost root extends FULL parent
|
|
@@ -722,8 +840,8 @@ export class ProtoArray {
|
|
|
722
840
|
return false;
|
|
723
841
|
}
|
|
724
842
|
|
|
725
|
-
// Condition 1: Payload is timely
|
|
726
|
-
if (this.isPayloadTimely(blockRoot)) {
|
|
843
|
+
// Condition 1: Payload is timely AND data is available
|
|
844
|
+
if (this.isPayloadTimely(blockRoot) && this.isPayloadDataAvailable(blockRoot)) {
|
|
727
845
|
return true;
|
|
728
846
|
}
|
|
729
847
|
|
|
@@ -1131,7 +1249,9 @@ export class ProtoArray {
|
|
|
1131
1249
|
this.indices.delete(root);
|
|
1132
1250
|
// Prune PTC votes for this block to prevent memory leak
|
|
1133
1251
|
// Spec: gloas/fork-choice.md (implicit - finalized blocks don't need PTC votes)
|
|
1134
|
-
this.
|
|
1252
|
+
this.payloadTimelinessVotes.delete(root);
|
|
1253
|
+
this.ptcAttested.delete(root);
|
|
1254
|
+
this.payloadDataAvailabilityVotes.delete(root);
|
|
1135
1255
|
}
|
|
1136
1256
|
|
|
1137
1257
|
// Store nodes prior to finalization
|