-
-
Notifications
You must be signed in to change notification settings - Fork 317
/
Copy pathpayload_store.ts
223 lines (184 loc) Β· 7.64 KB
/
payload_store.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
import {ApiClient} from "@lodestar/api";
import {ForkName} from "@lodestar/params";
import {ExecutionPayload, LightClientHeader} from "@lodestar/types";
import {Logger} from "@lodestar/utils";
import {MAX_PAYLOAD_HISTORY} from "../constants.js";
import {fetchBlock, getExecutionPayloadForBlockNumber} from "../utils/consensus.js";
import {bufferToHex, hexToNumber} from "../utils/conversion.js";
import {OrderedMap} from "./ordered_map.js";
type BlockELRoot = string;
type BlockELRootAndSlot = {
blockELRoot: BlockELRoot;
slot: number;
};
type BlockCLRoot = string;
/**
* The in-memory store for the execution payloads to be used to verify the proofs
*/
export class PayloadStore {
// We store the block root from execution for finalized blocks
// As these blocks are finalized, so not to be worried about conflicting roots
private finalizedRoots = new OrderedMap<BlockELRootAndSlot>();
// Unfinalized blocks may change over time and may have conflicting roots
// We can receive multiple light-client headers for the same block of execution
// So we why store unfinalized payloads by their CL root, which is only used
// in processing the light-client headers
private unfinalizedRoots = new Map<BlockCLRoot, BlockELRoot>();
// Payloads store with BlockELRoot as key
private payloads = new Map<BlockELRoot, ExecutionPayload>();
private latestBlockRoot: BlockELRoot | null = null;
constructor(private opts: {api: ApiClient; logger: Logger}) {}
get finalized(): ExecutionPayload | undefined {
const maxBlockNumberForFinalized = this.finalizedRoots.max;
if (maxBlockNumberForFinalized === undefined) {
return undefined;
}
const finalizedMaxRoot = this.finalizedRoots.get(maxBlockNumberForFinalized);
if (finalizedMaxRoot) {
return this.payloads.get(finalizedMaxRoot.blockELRoot);
}
return undefined;
}
get latest(): ExecutionPayload | undefined {
if (this.latestBlockRoot) {
return this.payloads.get(this.latestBlockRoot);
}
return undefined;
}
async get(blockId: number | string): Promise<ExecutionPayload | undefined> {
// Given block id is a block hash in hex (32 bytes root takes 64 hex chars + 2 for 0x prefix)
if (typeof blockId === "string" && blockId.startsWith("0x") && blockId.length === 64 + 2) {
return this.payloads.get(blockId);
}
// Given block id is a block number in hex
if (typeof blockId === "string" && blockId.startsWith("0x")) {
return this.getOrFetchFinalizedPayload(hexToNumber(blockId));
}
// Given block id is a block number in decimal string
if (typeof blockId === "string" && !blockId.startsWith("0x")) {
return this.getOrFetchFinalizedPayload(parseInt(blockId, 10));
}
// Given block id is a block number in decimal
if (typeof blockId === "number") {
return this.getOrFetchFinalizedPayload(blockId);
}
return undefined;
}
protected async getOrFetchFinalizedPayload(blockNumber: number): Promise<ExecutionPayload | undefined> {
const maxBlockNumberForFinalized = this.finalizedRoots.max;
const minBlockNumberForFinalized = this.finalizedRoots.min;
if (maxBlockNumberForFinalized === undefined || minBlockNumberForFinalized === undefined) {
return;
}
if (blockNumber > maxBlockNumberForFinalized) {
throw new Error(
`Block number ${blockNumber} is higher than the latest finalized block number. We recommend to use block hash for unfinalized blocks.`
);
}
let blockELRoot = this.finalizedRoots.get(blockNumber);
// check if we have payload cached locally else fetch from api
if (!blockELRoot) {
const finalizedMaxRoot = this.finalizedRoots.get(maxBlockNumberForFinalized);
const slot = finalizedMaxRoot?.slot;
if (slot !== undefined) {
const payloads = await getExecutionPayloadForBlockNumber(this.opts.api, slot, blockNumber);
for (const [slot, payload] of payloads.entries()) {
this.set(payload, slot, true);
}
}
}
blockELRoot = this.finalizedRoots.get(blockNumber);
if (blockELRoot) {
return this.payloads.get(blockELRoot.blockELRoot);
}
return undefined;
}
set(payload: ExecutionPayload, slot: number, finalized: boolean): void {
const blockELRoot = bufferToHex(payload.blockHash);
this.payloads.set(blockELRoot, payload);
if (this.latestBlockRoot) {
const latestPayload = this.payloads.get(this.latestBlockRoot);
if (latestPayload && latestPayload.blockNumber < payload.blockNumber) {
this.latestBlockRoot = blockELRoot;
}
} else {
this.latestBlockRoot = blockELRoot;
}
if (finalized) {
this.finalizedRoots.set(payload.blockNumber, {blockELRoot, slot});
}
}
async processLCHeader(header: LightClientHeader<ForkName.capella>, finalized = false): Promise<void> {
const blockSlot = header.beacon.slot;
const blockNumber = header.execution.blockNumber;
const blockELRoot = bufferToHex(header.execution.blockHash);
const blockCLRoot = bufferToHex(header.beacon.stateRoot);
const existingELRoot = this.unfinalizedRoots.get(blockCLRoot);
// ==== Finalized blocks ====
// if the block is finalized, we need to update the finalizedRoots map
if (finalized) {
this.finalizedRoots.set(blockNumber, {blockELRoot, slot: blockSlot});
// If the block is finalized and we already have the payload
// We can remove it from the unfinalizedRoots map and do nothing else
if (existingELRoot) {
this.unfinalizedRoots.delete(blockCLRoot);
}
// If the block is finalized and we do not have the payload
// We need to fetch and set the payload
else {
const block = await fetchBlock(this.opts.api, blockSlot);
if (block) {
this.payloads.set(blockELRoot, block.message.body.executionPayload);
} else {
this.opts.logger.error("Failed to fetch block", blockSlot);
}
}
return;
}
// ==== Unfinalized blocks ====
// We already have the payload for this block
if (existingELRoot && existingELRoot === blockELRoot) {
return;
}
// Re-org happened, we need to update the payload
if (existingELRoot && existingELRoot !== blockELRoot) {
this.payloads.delete(existingELRoot);
}
// This is unfinalized header we need to store it's root related to cl root
this.unfinalizedRoots.set(blockCLRoot, blockELRoot);
// We do not have the payload for this block, we need to fetch it
const block = await fetchBlock(this.opts.api, blockSlot);
if (block) {
this.set(block.message.body.executionPayload, blockSlot, false);
} else {
this.opts.logger.error("Failed to fetch finalized block", blockSlot);
}
this.prune();
}
prune(): void {
if (this.finalizedRoots.size <= MAX_PAYLOAD_HISTORY) return;
// Store doe not have any finalized blocks means it's recently initialized
if (this.finalizedRoots.max === undefined || this.finalizedRoots.min === undefined) return;
for (
let blockNumber = this.finalizedRoots.max - MAX_PAYLOAD_HISTORY;
blockNumber >= this.finalizedRoots.min;
blockNumber--
) {
const blockELRoot = this.finalizedRoots.get(blockNumber);
if (blockELRoot) {
this.payloads.delete(blockELRoot.blockELRoot);
this.finalizedRoots.delete(blockNumber);
}
}
for (const [clRoot, elRoot] of this.unfinalizedRoots) {
const payload = this.payloads.get(elRoot);
if (!payload) {
this.unfinalizedRoots.delete(clRoot);
continue;
}
if (payload.blockNumber < this.finalizedRoots.min) {
this.unfinalizedRoots.delete(clRoot);
}
}
}
}