Skip to content

Commit 983ef10

Browse files
authored
feat: add proposer duties v2 endpoint (#8597)
Adds proposer duties v2 endpoint which works the same as v1 but uses previous epoch to determine dependent root to account for deterministic proposer lookahead changes in fulu. ethereum/beacon-APIs#563
1 parent 6d7c41d commit 983ef10

File tree

4 files changed

+65
-6
lines changed

4 files changed

+65
-6
lines changed

packages/api/src/beacon/routes/validator.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,33 @@ export type Endpoints = {
285285
ExecutionOptimisticAndDependentRootMeta
286286
>;
287287

288+
/**
289+
* Get block proposers duties
290+
* Request beacon node to provide all validators that are scheduled to propose a block in the given epoch.
291+
* Duties should only need to be checked once per epoch, however a chain reorganization could occur that results in a change of duties.
292+
* For full safety, you should monitor head events and confirm the dependent root in this response matches. After Fulu, different checks
293+
* need to be performed as the dependent root changes due to deterministic proposer lookahead.
294+
*
295+
* Before Fulu:
296+
* - event.current_duty_dependent_root when `compute_epoch_at_slot(event.slot) == epoch`
297+
* - event.block otherwise
298+
* - dependent_root value is `get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch) - 1)`
299+
*
300+
* After Fulu:
301+
* - event.previous_duty_dependent_root when `compute_epoch_at_slot(event.slot) == epoch`
302+
* - event.block otherwise
303+
* - dependent_root value is `get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 1) - 1)`
304+
*
305+
* The dependent_root value is the genesis block root in the case of underflow."
306+
*/
307+
getProposerDutiesV2: Endpoint<
308+
"GET",
309+
{epoch: Epoch},
310+
{params: {epoch: Epoch}},
311+
ProposerDutyList,
312+
ExecutionOptimisticAndDependentRootMeta
313+
>;
314+
288315
getSyncCommitteeDuties: Endpoint<
289316
"POST",
290317
{
@@ -565,6 +592,21 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
565592
meta: ExecutionOptimisticAndDependentRootCodec,
566593
},
567594
},
595+
getProposerDutiesV2: {
596+
url: "/eth/v2/validator/duties/proposer/{epoch}",
597+
method: "GET",
598+
req: {
599+
writeReq: ({epoch}) => ({params: {epoch}}),
600+
parseReq: ({params}) => ({epoch: params.epoch}),
601+
schema: {
602+
params: {epoch: Schema.UintRequired},
603+
},
604+
},
605+
resp: {
606+
data: ProposerDutyListType,
607+
meta: ExecutionOptimisticAndDependentRootCodec,
608+
},
609+
},
568610
getSyncCommitteeDuties: {
569611
url: "/eth/v1/validator/duties/sync/{epoch}",
570612
method: "POST",

packages/api/test/unit/beacon/testData/validator.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ export const testData: GenericServerTestCases<Endpoints> = {
3535
meta: {executionOptimistic: true, dependentRoot: ZERO_HASH_HEX},
3636
},
3737
},
38+
getProposerDutiesV2: {
39+
args: {epoch: 1000},
40+
res: {
41+
data: [{slot: 1, validatorIndex: 2, pubkey: new Uint8Array(48).fill(3)}],
42+
meta: {executionOptimistic: true, dependentRoot: ZERO_HASH_HEX},
43+
},
44+
},
3845
getSyncCommitteeDuties: {
3946
args: {epoch: 1000, indices: [1, 2, 3]},
4047
res: {

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,7 +1002,7 @@ export function getValidatorApi(
10021002
return {data: contribution};
10031003
},
10041004

1005-
async getProposerDuties({epoch}) {
1005+
async getProposerDuties({epoch}, _context, opts?: {v2?: boolean}) {
10061006
notWhileSyncing();
10071007

10081008
// Early check that epoch is no more than current_epoch + 1, or allow for pre-genesis
@@ -1106,7 +1106,10 @@ export function getValidatorApi(
11061106

11071107
// Returns `null` on the one-off scenario where the genesis block decides its own shuffling.
11081108
// It should be set to the latest block applied to `self` or the genesis block root.
1109-
const dependentRoot = proposerShufflingDecisionRoot(state) || (await getGenesisBlockRoot(state));
1109+
const dependentRoot =
1110+
// In v2 the dependent root is different after fulu due to deterministic proposer lookahead
1111+
proposerShufflingDecisionRoot(opts?.v2 ? config.getForkName(startSlot) : ForkName.phase0, state) ||
1112+
(await getGenesisBlockRoot(state));
11101113

11111114
return {
11121115
data: duties,
@@ -1117,6 +1120,10 @@ export function getValidatorApi(
11171120
};
11181121
},
11191122

1123+
async getProposerDutiesV2(args, context) {
1124+
return this.getProposerDuties(args, context, {v2: true});
1125+
},
1126+
11201127
async getAttesterDuties({epoch, indices}) {
11211128
notWhileSyncing();
11221129

packages/state-transition/src/util/shufflingDecisionRoot.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {ForkName, isForkPostFulu} from "@lodestar/params";
12
import {Epoch, Root, Slot} from "@lodestar/types";
23
import {CachedBeaconStateAllForks} from "../types.js";
34
import {getBlockRootAtSlot} from "./blockRoot.js";
@@ -10,8 +11,8 @@ import {computeStartSlotAtEpoch} from "./epoch.js";
1011
* Returns `null` on the one-off scenario where the genesis block decides its own shuffling.
1112
* It should be set to the latest block applied to this `state` or the genesis block root.
1213
*/
13-
export function proposerShufflingDecisionRoot(state: CachedBeaconStateAllForks): Root | null {
14-
const decisionSlot = proposerShufflingDecisionSlot(state);
14+
export function proposerShufflingDecisionRoot(fork: ForkName, state: CachedBeaconStateAllForks): Root | null {
15+
const decisionSlot = proposerShufflingDecisionSlot(fork, state);
1516
if (state.slot === decisionSlot) {
1617
return null;
1718
}
@@ -22,8 +23,10 @@ export function proposerShufflingDecisionRoot(state: CachedBeaconStateAllForks):
2223
* Returns the slot at which the proposer shuffling was decided. The block root at this slot
2324
* can be used to key the proposer shuffling for the current epoch.
2425
*/
25-
function proposerShufflingDecisionSlot(state: CachedBeaconStateAllForks): Slot {
26-
const startSlot = computeStartSlotAtEpoch(state.epochCtx.epoch);
26+
function proposerShufflingDecisionSlot(fork: ForkName, state: CachedBeaconStateAllForks): Slot {
27+
// After fulu, the decision slot is in previous epoch due to deterministic proposer lookahead
28+
const epoch = isForkPostFulu(fork) ? state.epochCtx.epoch - 1 : state.epochCtx.epoch;
29+
const startSlot = computeStartSlotAtEpoch(epoch);
2730
return Math.max(startSlot - 1, 0);
2831
}
2932

0 commit comments

Comments
 (0)