Skip to content

Commit fdfdafc

Browse files
committed
merge: resolve conflicts merging epbs-devnet-0 into te/epbs-devnet-0_syncing
Per Nico's requested flow: 1) branch from te/epbs-devnet-0_syncing (up to date, includes ChainSafe#8995 commit 6d6b2de) 2) merge epbs-devnet-0 (up to date) 3) resolve conflicts Conflicts resolved in: - ReqRespBeaconNode.ts - handlers/executionPayloadEnvelopesByRange.ts - reqresp/types.ts - forkChoice/interface.ts Also deduplicated merge duplicates in protocols/rateLimit/ssz type maps and aligned envelope handler callsites/tests. Build/check-types/lint pass locally.
2 parents 6d6b2de + 5db1b88 commit fdfdafc

13 files changed

Lines changed: 268 additions & 41 deletions

File tree

packages/beacon-node/src/api/impl/validator/index.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,17 +1007,18 @@ export function getValidatorApi(
10071007

10081008
let index: CommitteeIndex;
10091009
if (isForkPostGloas(fork)) {
1010-
// In Gloas (ePBS), attestation.data.index signals payload status in fork-choice:
1011-
// 0 = EMPTY / not present, 1 = FULL / present.
1012-
// Per spec, same-slot attestations must always use index = 0.
1013-
const canonicalAttestedBlock = chain.forkChoice.getCanonicalBlockClosestLteSlot(slot);
1014-
const isMatchingCanonicalRoot =
1015-
canonicalAttestedBlock !== null && canonicalAttestedBlock.blockRoot === toRootHex(beaconBlockRoot);
1016-
1017-
if (!isMatchingCanonicalRoot || canonicalAttestedBlock.slot === slot) {
1018-
index = 0;
1010+
const canonicalBlock = chain.forkChoice.getCanonicalBlockByRoot(beaconBlockRoot);
1011+
if (!canonicalBlock) {
1012+
// This should never happen
1013+
throw Error(`Block not found in fork choice for slot=${slot}, root=${toRootHex(beaconBlockRoot)}`);
1014+
}
1015+
// After Gloas, attestation.data.index signals payload status in fork-choice:
1016+
// - 0 = EMPTY / not present, 1 = FULL / present
1017+
// - same-slot attestations must always use index = 0
1018+
if (canonicalBlock.slot !== slot) {
1019+
index = canonicalBlock.payloadStatus === PayloadStatus.FULL ? 1 : 0;
10191020
} else {
1020-
index = canonicalAttestedBlock.payloadStatus === PayloadStatus.FULL ? 1 : 0;
1021+
index = 0;
10211022
}
10221023
} else if (isForkPostElectra(fork)) {
10231024
index = 0;

packages/beacon-node/src/network/reqresp/handlers/index.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,14 @@ export function getReqRespHandlers({db, chain}: {db: IBeaconDb; chain: IBeaconCh
6464
const body = DataColumnSidecarsByRootRequestType(chain.config).deserialize(req.data);
6565
return onDataColumnSidecarsByRoot(body, chain, db, peerId, peerClient);
6666
},
67+
[ReqRespMethod.ExecutionPayloadEnvelopesByRange]: (req, peerId, peerClient) => {
68+
const body = ssz.gloas.ExecutionPayloadEnvelopesByRangeRequest.deserialize(req.data);
69+
return onExecutionPayloadEnvelopesByRange(body, chain, db);
70+
},
6771
[ReqRespMethod.ExecutionPayloadEnvelopesByRoot]: (req) => {
6872
const body = ExecutionPayloadEnvelopesByRootRequestType(chain.config).deserialize(req.data);
6973
return onExecutionPayloadEnvelopesByRoot(body, chain, db);
7074
},
71-
[ReqRespMethod.ExecutionPayloadEnvelopesByRange]: (req) => {
72-
const body = ssz.gloas.ExecutionPayloadEnvelopesByRangeRequest.deserialize(req.data);
73-
return onExecutionPayloadEnvelopesByRange(body, chain, db);
74-
},
75-
7675
[ReqRespMethod.LightClientBootstrap]: (req) => {
7776
const body = ssz.Root.deserialize(req.data);
7877
return onLightClientBootstrap(body, chain);

packages/beacon-node/src/network/reqresp/protocols.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,14 @@ export const DataColumnSidecarsByRoot = toProtocol({
9494
contextBytesType: ContextBytesType.ForkDigest,
9595
});
9696

97-
export const ExecutionPayloadEnvelopesByRoot = toProtocol({
98-
method: ReqRespMethod.ExecutionPayloadEnvelopesByRoot,
97+
export const ExecutionPayloadEnvelopesByRange = toProtocol({
98+
method: ReqRespMethod.ExecutionPayloadEnvelopesByRange,
9999
version: Version.V1,
100100
contextBytesType: ContextBytesType.ForkDigest,
101101
});
102102

103-
export const ExecutionPayloadEnvelopesByRange = toProtocol({
104-
method: ReqRespMethod.ExecutionPayloadEnvelopesByRange,
103+
export const ExecutionPayloadEnvelopesByRoot = toProtocol({
104+
method: ReqRespMethod.ExecutionPayloadEnvelopesByRoot,
105105
version: Version.V1,
106106
contextBytesType: ContextBytesType.ForkDigest,
107107
});

packages/beacon-node/src/network/reqresp/rateLimit.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,24 +73,24 @@ export const rateLimitQuotas: (fork: ForkName, config: BeaconConfig) => Record<R
7373
req.reduce((total, item) => total + item.columns.length, 0)
7474
),
7575
},
76-
[ReqRespMethod.ExecutionPayloadEnvelopesByRoot]: {
77-
// Rationale: similar to BeaconBlocksByRoot — one envelope per block
76+
[ReqRespMethod.ExecutionPayloadEnvelopesByRange]: {
77+
// Rationale: bounded by payload request limits
7878
byPeer: {quota: config.MAX_REQUEST_PAYLOADS, quotaTimeMs: 10_000},
7979
getRequestCount: getRequestCountFn(
8080
fork,
8181
config,
82-
ReqRespMethod.ExecutionPayloadEnvelopesByRoot,
83-
(req) => req.length
82+
ReqRespMethod.ExecutionPayloadEnvelopesByRange,
83+
(req) => req.count
8484
),
8585
},
86-
[ReqRespMethod.ExecutionPayloadEnvelopesByRange]: {
87-
// Rationale: similar to BeaconBlocksByRange — one envelope per block
86+
[ReqRespMethod.ExecutionPayloadEnvelopesByRoot]: {
87+
// Rationale: similar to BeaconBlocksByRoot — one envelope per block
8888
byPeer: {quota: config.MAX_REQUEST_PAYLOADS, quotaTimeMs: 10_000},
8989
getRequestCount: getRequestCountFn(
9090
fork,
9191
config,
92-
ReqRespMethod.ExecutionPayloadEnvelopesByRange,
93-
(req) => req.count
92+
ReqRespMethod.ExecutionPayloadEnvelopesByRoot,
93+
(req) => req.length
9494
),
9595
},
9696
[ReqRespMethod.LightClientBootstrap]: {

packages/beacon-node/src/network/reqresp/score.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export function onOutgoingReqRespError(e: RequestError, method: ReqRespMethod):
5555
return PeerAction.LowToleranceError;
5656
case ReqRespMethod.BeaconBlocksByRange:
5757
case ReqRespMethod.BeaconBlocksByRoot:
58+
case ReqRespMethod.ExecutionPayloadEnvelopesByRange:
5859
return PeerAction.MidToleranceError;
5960
default:
6061
return null;
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import {describe, expect, it, vi} from "vitest";
2+
import {ForkName} from "@lodestar/params";
3+
import {ssz} from "@lodestar/types";
4+
import {toRootHex} from "@lodestar/utils";
5+
import {onExecutionPayloadEnvelopesByRange} from "../../../../src/network/reqresp/handlers/executionPayloadEnvelopesByRange.js";
6+
7+
function rootWithByte(n: number): Uint8Array {
8+
const root = new Uint8Array(32);
9+
root[31] = n;
10+
return root;
11+
}
12+
13+
describe("beacon-node / network / reqresp / handlers / executionPayloadEnvelopesByRange", () => {
14+
it("serves envelopes from finalized archive and non-finalized cache", async () => {
15+
const finalizedEnvelope = ssz.gloas.SignedExecutionPayloadEnvelope.defaultValue();
16+
finalizedEnvelope.message.slot = 64;
17+
finalizedEnvelope.message.beaconBlockRoot = rootWithByte(64);
18+
19+
const hotEnvelope = ssz.gloas.SignedExecutionPayloadEnvelope.defaultValue();
20+
hotEnvelope.message.slot = 65;
21+
hotEnvelope.message.beaconBlockRoot = rootWithByte(65);
22+
23+
const archivedBySlot = new Map<number, typeof finalizedEnvelope>([[64, finalizedEnvelope]]);
24+
const hotByRoot = new Map<string, typeof hotEnvelope>([
25+
[toRootHex(hotEnvelope.message.beaconBlockRoot), hotEnvelope],
26+
]);
27+
28+
const chain = {
29+
earliestAvailableSlot: 1,
30+
logger: {verbose: vi.fn()},
31+
config: {
32+
SLOTS_PER_EPOCH: 32,
33+
MAX_REQUEST_BLOCKS: 1024,
34+
MAX_REQUEST_BLOCKS_DENEB: 128,
35+
getForkName: () => ForkName.gloas,
36+
getForkBoundaryAtEpoch: (epoch: number) => ({fork: ForkName.gloas, epoch}),
37+
},
38+
forkChoice: {
39+
getFinalizedCheckpointSlot: () => 64,
40+
getHeadRoot: () => "0xhead",
41+
getBlockHexDefaultStatus: () => ({slot: 66, blockRoot: toRootHex(rootWithByte(66))}),
42+
getAllAncestorBlocks: () => [
43+
{slot: 66, blockRoot: toRootHex(rootWithByte(66))},
44+
{slot: 65, blockRoot: toRootHex(rootWithByte(65))},
45+
{slot: 64, blockRoot: toRootHex(rootWithByte(64))},
46+
],
47+
},
48+
} as any;
49+
50+
const db = {
51+
executionPayloadEnvelopeArchive: {
52+
get: vi.fn(async (slot: number) => archivedBySlot.get(slot) ?? null),
53+
},
54+
executionPayloadEnvelope: {
55+
get: vi.fn(async (root: Uint8Array) => hotByRoot.get(toRootHex(root)) ?? null),
56+
},
57+
} as any;
58+
59+
const request = {startSlot: 64, count: 2};
60+
61+
const responses = [];
62+
for await (const response of onExecutionPayloadEnvelopesByRange(request, chain, db)) {
63+
responses.push(ssz.gloas.SignedExecutionPayloadEnvelope.deserialize(response.data));
64+
}
65+
66+
expect(responses).toHaveLength(2);
67+
expect(responses.map((e) => e.message.slot)).toEqual([64, 65]);
68+
});
69+
70+
it("returns nothing for requests below earliestAvailableSlot", async () => {
71+
const chain = {
72+
earliestAvailableSlot: 10,
73+
logger: {verbose: vi.fn()},
74+
config: {
75+
SLOTS_PER_EPOCH: 32,
76+
MAX_REQUEST_BLOCKS: 1024,
77+
MAX_REQUEST_BLOCKS_DENEB: 128,
78+
getForkName: () => ForkName.gloas,
79+
getForkBoundaryAtEpoch: (epoch: number) => ({fork: ForkName.gloas, epoch}),
80+
},
81+
forkChoice: {
82+
getFinalizedCheckpointSlot: () => 0,
83+
getHeadRoot: () => "0xhead",
84+
getBlockHexDefaultStatus: () => undefined,
85+
getAllAncestorBlocks: () => [],
86+
},
87+
} as any;
88+
89+
const db = {
90+
executionPayloadEnvelopeArchive: {get: vi.fn()},
91+
executionPayloadEnvelope: {get: vi.fn()},
92+
} as any;
93+
94+
const request = {startSlot: 1, count: 1};
95+
96+
const responses = [];
97+
for await (const response of onExecutionPayloadEnvelopesByRange(request, chain, db)) {
98+
responses.push(response);
99+
}
100+
101+
expect(responses).toHaveLength(0);
102+
expect(chain.logger.verbose).toHaveBeenCalledTimes(1);
103+
expect(db.executionPayloadEnvelopeArchive.get).not.toHaveBeenCalled();
104+
expect(db.executionPayloadEnvelope.get).not.toHaveBeenCalled();
105+
});
106+
107+
it("includes head envelope when ancestor list excludes head", async () => {
108+
const finalizedEnvelope = ssz.gloas.SignedExecutionPayloadEnvelope.defaultValue();
109+
finalizedEnvelope.message.slot = 64;
110+
finalizedEnvelope.message.beaconBlockRoot = rootWithByte(64);
111+
112+
const headEnvelope = ssz.gloas.SignedExecutionPayloadEnvelope.defaultValue();
113+
headEnvelope.message.slot = 66;
114+
headEnvelope.message.beaconBlockRoot = rootWithByte(66);
115+
116+
const slot65Envelope = ssz.gloas.SignedExecutionPayloadEnvelope.defaultValue();
117+
slot65Envelope.message.slot = 65;
118+
slot65Envelope.message.beaconBlockRoot = rootWithByte(65);
119+
120+
const archivedBySlot = new Map<number, typeof finalizedEnvelope>([[64, finalizedEnvelope]]);
121+
const hotByRoot = new Map<string, typeof headEnvelope>([
122+
[toRootHex(headEnvelope.message.beaconBlockRoot), headEnvelope],
123+
[toRootHex(slot65Envelope.message.beaconBlockRoot), slot65Envelope],
124+
]);
125+
126+
const chain = {
127+
earliestAvailableSlot: 1,
128+
logger: {verbose: vi.fn()},
129+
config: {
130+
SLOTS_PER_EPOCH: 32,
131+
MAX_REQUEST_BLOCKS: 1024,
132+
MAX_REQUEST_BLOCKS_DENEB: 128,
133+
getForkName: () => ForkName.gloas,
134+
getForkBoundaryAtEpoch: (epoch: number) => ({fork: ForkName.gloas, epoch}),
135+
},
136+
forkChoice: {
137+
getFinalizedCheckpointSlot: () => 64,
138+
getHeadRoot: () => "0xhead",
139+
getBlockHexDefaultStatus: () => ({slot: 66, blockRoot: toRootHex(rootWithByte(66))}),
140+
getAllAncestorBlocks: () => [
141+
{slot: 65, blockRoot: toRootHex(rootWithByte(65))},
142+
{slot: 64, blockRoot: toRootHex(rootWithByte(64))},
143+
],
144+
},
145+
} as any;
146+
147+
const db = {
148+
executionPayloadEnvelopeArchive: {
149+
get: vi.fn(async (slot: number) => archivedBySlot.get(slot) ?? null),
150+
},
151+
executionPayloadEnvelope: {
152+
get: vi.fn(async (root: Uint8Array) => hotByRoot.get(toRootHex(root)) ?? null),
153+
},
154+
} as any;
155+
156+
const request = {startSlot: 64, count: 3};
157+
158+
const responses = [];
159+
for await (const response of onExecutionPayloadEnvelopesByRange(request, chain, db)) {
160+
responses.push(ssz.gloas.SignedExecutionPayloadEnvelope.deserialize(response.data));
161+
}
162+
163+
expect(responses.map((e) => e.message.slot)).toEqual([64, 65, 66]);
164+
});
165+
});

packages/fork-choice/src/forkChoice/forkChoice.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,6 +1239,21 @@ export class ForkChoice implements IForkChoice {
12391239
};
12401240
}
12411241

1242+
getCanonicalBlockByRoot(blockRoot: Root): ProtoBlock | null {
1243+
const blockRootHex = toRootHex(blockRoot);
1244+
if (blockRootHex === this.head.blockRoot) {
1245+
return this.head;
1246+
}
1247+
1248+
for (const block of this.protoArray.iterateAncestorNodes(this.head.blockRoot)) {
1249+
if (block.blockRoot === blockRootHex) {
1250+
return block;
1251+
}
1252+
}
1253+
1254+
return null;
1255+
}
1256+
12421257
getCanonicalBlockAtSlot(slot: Slot): ProtoBlock | null {
12431258
if (slot > this.head.slot) {
12441259
return null;

packages/fork-choice/src/forkChoice/interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ export interface IForkChoice {
268268
blockRoot: RootHex,
269269
payloadStatus: PayloadStatus
270270
): {ancestors: ProtoBlock[]; nonAncestors: ProtoBlock[]};
271+
getCanonicalBlockByRoot(blockRoot: Root): ProtoBlock | null;
271272
getCanonicalBlockAtSlot(slot: Slot): ProtoBlock | null;
272273
getCanonicalBlockClosestLteSlot(slot: Slot): ProtoBlock | null;
273274
/**

0 commit comments

Comments
 (0)