@lodestar/builder 1.48.0-dev.57c1e5614b → 1.48.0-dev.59456ca330

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.
@@ -0,0 +1,172 @@
1
+ import { ApiError, routes } from "@lodestar/api";
2
+ import { isForkPostGloas } from "@lodestar/params";
3
+ import { isGloasBeaconBlock } from "@lodestar/types";
4
+ import { TimeoutError, isErrorAborted, isFetchError, pruneSetToMax, retry, toRootHex } from "@lodestar/utils";
5
+ const { EventType } = routes.events;
6
+ /**
7
+ * Observes imported beacon blocks through the source beacon node API.
8
+ *
9
+ * Each block root is evaluated at most once while retained in the bounded seen set. Terminal failures remain consumed
10
+ * until normal FIFO eviction; reconnect, replay, and recovery policies are handled separately.
11
+ */
12
+ export class BlockObserver {
13
+ config;
14
+ logger;
15
+ api;
16
+ fns = [];
17
+ seenBlockRoots = new Set();
18
+ retries;
19
+ retryDelay;
20
+ maxSeenBlockRoots;
21
+ constructor(config, logger, api, { retries = 5, retryDelay = 200, maxSeenBlockRoots = 256 } = {}) {
22
+ this.config = config;
23
+ this.logger = logger;
24
+ this.api = api;
25
+ this.retries = retries;
26
+ this.retryDelay = retryDelay;
27
+ this.maxSeenBlockRoots = maxSeenBlockRoots;
28
+ }
29
+ /**
30
+ * Register a block consumer. Consumers that require complete observation must register before {@link start}, because
31
+ * roots observed before registration remain consumed until normal FIFO eviction.
32
+ */
33
+ runOnBlock(fn) {
34
+ this.fns.push(fn);
35
+ }
36
+ start(signal) {
37
+ this.logger.info("Subscribing to block events", {
38
+ retries: this.retries,
39
+ retryDelay: this.retryDelay,
40
+ maxSeenBlockRoots: this.maxSeenBlockRoots,
41
+ });
42
+ this.api.events
43
+ .eventstream({
44
+ topics: [EventType.block],
45
+ signal,
46
+ onEvent: (event) => {
47
+ if (event.type !== EventType.block) {
48
+ this.logger.debug("Ignoring unexpected beacon event", { eventType: event.type });
49
+ return;
50
+ }
51
+ void this.processBlockEvent(event.message, signal);
52
+ },
53
+ onError: (error) => {
54
+ this.logger.error("Failed to receive block event", {}, error);
55
+ },
56
+ onClose: () => {
57
+ if (signal.aborted) {
58
+ this.logger.debug("Closed stream for block events");
59
+ }
60
+ else {
61
+ this.logger.error("Block event stream closed unexpectedly", {});
62
+ }
63
+ },
64
+ })
65
+ .catch((error) => {
66
+ this.logger.error("Failed to subscribe to block events", {}, error instanceof Error ? error : Error(String(error)));
67
+ });
68
+ }
69
+ async processBlockEvent(event, signal) {
70
+ try {
71
+ const { slot, block: blockRoot, executionOptimistic } = event;
72
+ if (this.seenBlockRoots.has(blockRoot)) {
73
+ this.logger.debug("Ignoring duplicate block event", { slot, blockRoot });
74
+ return;
75
+ }
76
+ this.seenBlockRoots.add(blockRoot);
77
+ pruneSetToMax(this.seenBlockRoots, this.maxSeenBlockRoots);
78
+ if (!isForkPostGloas(this.config.getForkName(slot))) {
79
+ this.logger.debug("Ignoring pre-Gloas block event", { slot, blockRoot });
80
+ return;
81
+ }
82
+ const response = await retry(async () => {
83
+ // TODO GLOAS: Remove this lookup once bid-selection notifications provide the required data.
84
+ const result = await this.api.beacon.getBlockV2({ blockId: blockRoot }, { signal });
85
+ result.assertOk();
86
+ return result;
87
+ }, {
88
+ retries: this.retries,
89
+ retryDelay: this.retryDelay,
90
+ signal,
91
+ shouldRetry: isRetryableBlockRetrievalError,
92
+ onRetry: (error, attempt) => {
93
+ this.logger.debug("Retrying block retrieval", {
94
+ slot,
95
+ blockRoot,
96
+ attempt,
97
+ error: error.message,
98
+ });
99
+ },
100
+ }).catch((error) => {
101
+ if (!isErrorAborted(error)) {
102
+ this.logger.error("Failed to retrieve block referenced by block event", { slot, blockRoot }, error instanceof Error ? error : Error(String(error)));
103
+ }
104
+ return null;
105
+ });
106
+ if (response === null) {
107
+ return;
108
+ }
109
+ const { version } = response.meta();
110
+ if (!isForkPostGloas(version)) {
111
+ this.logger.warn("Ignoring block with unsupported fork version", { slot, blockRoot, fork: version });
112
+ return;
113
+ }
114
+ const block = response.value();
115
+ if (!isGloasBeaconBlock(block.message)) {
116
+ this.logger.warn("Block response version and body do not agree", { slot, blockRoot, fork: version });
117
+ return;
118
+ }
119
+ if (block.message.slot !== slot) {
120
+ this.logger.warn("Block response slot does not match block event", {
121
+ slot,
122
+ blockRoot,
123
+ blockSlot: block.message.slot,
124
+ });
125
+ return;
126
+ }
127
+ const postGloasBlock = block;
128
+ const signedBid = postGloasBlock.message.body.signedExecutionPayloadBid;
129
+ const observedBlock = {
130
+ blockRoot,
131
+ slot,
132
+ executionOptimistic,
133
+ version,
134
+ block: postGloasBlock,
135
+ signedBid,
136
+ };
137
+ this.logger.debug("Observed post-Gloas block", {
138
+ slot,
139
+ blockRoot,
140
+ fork: version,
141
+ builderIndex: signedBid.message.builderIndex,
142
+ value: signedBid.message.value,
143
+ blockHash: toRootHex(signedBid.message.blockHash),
144
+ parentBlockHash: toRootHex(signedBid.message.parentBlockHash),
145
+ });
146
+ await Promise.all(this.fns.map(async (fn) => {
147
+ try {
148
+ await fn(observedBlock);
149
+ }
150
+ catch (error) {
151
+ if (isErrorAborted(error)) {
152
+ return;
153
+ }
154
+ this.logger.error("Failed to process observed block", { slot, blockRoot }, error instanceof Error ? error : Error(String(error)));
155
+ }
156
+ }));
157
+ }
158
+ catch (error) {
159
+ if (isErrorAborted(error)) {
160
+ return;
161
+ }
162
+ this.logger.error("Failed to process block event", { slot: event.slot, blockRoot: event.block }, error instanceof Error ? error : Error(String(error)));
163
+ }
164
+ }
165
+ }
166
+ export function isRetryableBlockRetrievalError(error) {
167
+ if (error instanceof ApiError) {
168
+ return error.status === 404 || error.status >= 500;
169
+ }
170
+ return error instanceof TimeoutError || (isFetchError(error) && error.type !== "input");
171
+ }
172
+ //# sourceMappingURL=blockObserver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blockObserver.js","sourceRoot":"","sources":["../../src/services/blockObserver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import { ForkPostGloas } from "@lodestar/params";
2
+ import { BlobsBundle, ExecutionPayload, Root, RootHex, Slot, gloas } from "@lodestar/types";
3
+ export type BuiltPayload = {
4
+ sourceId: string;
5
+ executionPayload: ExecutionPayload<ForkPostGloas>;
6
+ executionRequests: gloas.ExecutionRequests;
7
+ blobsBundle: BlobsBundle<ForkPostGloas>;
8
+ /** Value of the payload to the fee recipient in wei, as reported by the execution client */
9
+ executionPayloadValue: bigint;
10
+ };
11
+ export type StoredPayload = {
12
+ slot: Slot;
13
+ parentBlockRoot: Root;
14
+ blockHash: RootHex;
15
+ payload: BuiltPayload;
16
+ };
17
+ export declare class PayloadStore {
18
+ private readonly byBlockHash;
19
+ add(payload: StoredPayload): void;
20
+ get(blockHash: RootHex): StoredPayload | null;
21
+ has(blockHash: RootHex): boolean;
22
+ prune(currentSlot: Slot): void;
23
+ get size(): number;
24
+ }
25
+ //# sourceMappingURL=payloadStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payloadStore.d.ts","sourceRoot":"","sources":["../../src/services/payloadStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,aAAa,EAAC,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAC,WAAW,EAAE,gBAAgB,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAC,MAAM,iBAAiB,CAAC;AAI1F,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,gBAAgB,CAAC,aAAa,CAAC,CAAC;IAClD,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,CAAC;IAC3C,WAAW,EAAE,WAAW,CAAC,aAAa,CAAC,CAAC;IACxC,4FAA4F;IAC5F,qBAAqB,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,IAAI,CAAC;IACX,eAAe,EAAE,IAAI,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,YAAY,CAAC;CACvB,CAAC;AAKF,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqC;IAEjE,GAAG,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAEhC;IAED,GAAG,CAAC,SAAS,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CAE5C;IAED,GAAG,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAE/B;IAED,KAAK,CAAC,WAAW,EAAE,IAAI,GAAG,IAAI,CAM7B;IAED,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
@@ -0,0 +1,25 @@
1
+ /** Keep payloads for this many slots after their target slot, late blocks may still commit to them */
2
+ const KEEP_SLOTS = 2;
3
+ export class PayloadStore {
4
+ byBlockHash = new Map();
5
+ add(payload) {
6
+ this.byBlockHash.set(payload.blockHash, payload);
7
+ }
8
+ get(blockHash) {
9
+ return this.byBlockHash.get(blockHash) ?? null;
10
+ }
11
+ has(blockHash) {
12
+ return this.byBlockHash.has(blockHash);
13
+ }
14
+ prune(currentSlot) {
15
+ for (const [blockHash, { slot }] of this.byBlockHash) {
16
+ if (slot + KEEP_SLOTS < currentSlot) {
17
+ this.byBlockHash.delete(blockHash);
18
+ }
19
+ }
20
+ }
21
+ get size() {
22
+ return this.byBlockHash.size;
23
+ }
24
+ }
25
+ //# sourceMappingURL=payloadStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payloadStore.js","sourceRoot":"","sources":["../../src/services/payloadStore.ts"],"names":[],"mappings":"AAqBA,sGAAsG;AACtG,MAAM,UAAU,GAAG,CAAC,CAAC;AAErB,MAAM,OAAO,YAAY;IACN,WAAW,GAAG,IAAI,GAAG,EAA0B,CAAC;IAEjE,GAAG,CAAC,OAAsB;QACxB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACnD,CAAC;IAED,GAAG,CAAC,SAAkB;QACpB,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC;IACjD,CAAC;IAED,GAAG,CAAC,SAAkB;QACpB,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACzC,CAAC;IAED,KAAK,CAAC,WAAiB;QACrB,KAAK,MAAM,CAAC,SAAS,EAAE,EAAC,IAAI,EAAC,CAAC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACnD,IAAI,IAAI,GAAG,UAAU,GAAG,WAAW,EAAE,CAAC;gBACpC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YACrC,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;IAC/B,CAAC;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodestar/builder",
3
- "version": "1.48.0-dev.57c1e5614b",
3
+ "version": "1.48.0-dev.59456ca330",
4
4
  "description": "A Typescript implementation of the builder client",
5
5
  "author": "ChainSafe Systems",
6
6
  "license": "Apache-2.0",
@@ -48,16 +48,16 @@
48
48
  "dependencies": {
49
49
  "@chainsafe/lodestar-z": "catalog:",
50
50
  "@chainsafe/ssz": "^1.7.0",
51
- "@lodestar/api": "^1.48.0-dev.57c1e5614b",
52
- "@lodestar/config": "^1.48.0-dev.57c1e5614b",
53
- "@lodestar/params": "^1.48.0-dev.57c1e5614b",
54
- "@lodestar/state-transition": "^1.48.0-dev.57c1e5614b",
55
- "@lodestar/types": "^1.48.0-dev.57c1e5614b",
56
- "@lodestar/utils": "^1.48.0-dev.57c1e5614b"
51
+ "@lodestar/api": "^1.48.0-dev.59456ca330",
52
+ "@lodestar/config": "^1.48.0-dev.59456ca330",
53
+ "@lodestar/params": "^1.48.0-dev.59456ca330",
54
+ "@lodestar/state-transition": "^1.48.0-dev.59456ca330",
55
+ "@lodestar/types": "^1.48.0-dev.59456ca330",
56
+ "@lodestar/utils": "^1.48.0-dev.59456ca330"
57
57
  },
58
58
  "devDependencies": {
59
- "@lodestar/logger": "^1.48.0-dev.57c1e5614b",
60
- "@lodestar/test-utils": "^1.48.0-dev.57c1e5614b"
59
+ "@lodestar/logger": "^1.48.0-dev.59456ca330",
60
+ "@lodestar/test-utils": "^1.48.0-dev.59456ca330"
61
61
  },
62
- "gitHead": "998308c37292db4af199b9d67be2a4866887da98"
62
+ "gitHead": "a0db83f84e4d5ea344e8a204cd14eea587d6ce66"
63
63
  }
package/src/builder.ts CHANGED
@@ -7,15 +7,19 @@ import {waitForGenesis} from "./genesis.js";
7
7
  import {resolveBuilderIdentity} from "./identity.js";
8
8
  import {Metrics} from "./metrics.js";
9
9
  import {logNodeVersion, waitForNodeReady} from "./readiness.js";
10
+ import {BlockObserver} from "./services/blockObserver.js";
10
11
  import {BuilderSigner, Keypair} from "./services/builderSigner.js";
11
12
  import {BuilderStatusTracker} from "./services/builderStatusTracker.js";
13
+ import {PayloadStore} from "./services/payloadStore.js";
12
14
 
13
15
  export type BuilderModules = {
14
16
  opts: BuilderOptions;
15
17
  builderSigner: BuilderSigner;
18
+ blockObserver: BlockObserver;
16
19
  builderStatusTracker: BuilderStatusTracker;
17
20
  clock: IClock;
18
21
  index: BuilderIndex;
22
+ store: PayloadStore;
19
23
  };
20
24
 
21
25
  export type BuilderOptions = {
@@ -34,25 +38,31 @@ export type BuilderOptions = {
34
38
  */
35
39
  export class Builder {
36
40
  readonly builderSigner: BuilderSigner;
41
+ private readonly blockObserver: BlockObserver;
37
42
  private readonly builderStatusTracker: BuilderStatusTracker;
38
43
  private readonly controller: AbortController;
39
44
  private readonly clock: IClock;
40
45
  private readonly index: BuilderIndex;
41
46
  private readonly logger: Logger;
42
47
  private readonly executionFeeRecipient: ExecutionAddress;
48
+ private readonly store: PayloadStore;
43
49
 
44
- constructor({opts, builderSigner, builderStatusTracker, clock, index}: BuilderModules) {
50
+ constructor({opts, builderSigner, blockObserver, builderStatusTracker, clock, index, store}: BuilderModules) {
45
51
  this.builderSigner = builderSigner;
52
+ this.blockObserver = blockObserver;
46
53
  this.builderStatusTracker = builderStatusTracker;
47
54
  this.clock = clock;
48
55
  this.controller = opts.abortController;
49
56
  this.logger = opts.logger;
50
57
  this.index = index;
58
+ this.store = store;
51
59
 
52
60
  this.executionFeeRecipient = opts.executionFeeRecipient;
53
61
 
62
+ this.clock.runEverySlot(async (slot) => this.onSlot(slot));
54
63
  this.clock.runEveryEpoch((epoch) => this.builderStatusTracker.poll(epoch));
55
64
  this.clock.start(this.controller.signal);
65
+ this.blockObserver.start(this.controller.signal);
56
66
 
57
67
  this.logger.info("Builder client initialized", {
58
68
  index: this.index,
@@ -89,8 +99,15 @@ export class Builder {
89
99
  );
90
100
 
91
101
  const builderStatusTracker = new BuilderStatusTracker(api, logger, index, opts.metrics);
102
+ const blockObserver = new BlockObserver(config, logger, api);
92
103
 
93
- return new Builder({opts, builderSigner, builderStatusTracker, clock, index});
104
+ const store = new PayloadStore();
105
+
106
+ return new Builder({opts, builderSigner, blockObserver, builderStatusTracker, clock, index, store});
107
+ }
108
+
109
+ private async onSlot(slot: number): Promise<void> {
110
+ this.store.prune(slot);
94
111
  }
95
112
 
96
113
  async close(): Promise<void> {
package/src/identity.ts CHANGED
@@ -1,12 +1,10 @@
1
1
  import {ApiClient, ApiError, HttpStatusCode, routes} from "@lodestar/api";
2
2
  import {ChainForkConfig} from "@lodestar/config";
3
3
  import {PAYLOAD_BUILDER_VERSION} from "@lodestar/params";
4
- import {IClock} from "@lodestar/state-transition";
4
+ import {IClock, computeStartSlotAtEpoch} from "@lodestar/state-transition";
5
5
  import {BuilderIndex, BuilderStatus} from "@lodestar/types";
6
6
  import {ErrorAborted, Logger, TimeoutError, isFetchError, sleep, toHex} from "@lodestar/utils";
7
7
 
8
- export const WAITING_FOR_BUILDER_POLL_MS = 10 * 1000;
9
-
10
8
  export async function resolveBuilderIdentity(
11
9
  api: ApiClient,
12
10
  logger: Logger,
@@ -71,7 +69,7 @@ async function waitForBuilder(
71
69
  currentEpoch,
72
70
  slot: clock.getCurrentSlot(),
73
71
  });
74
- await sleep(WAITING_FOR_BUILDER_POLL_MS, signal);
72
+ await sleep(msToNextEpochPoll(clock), signal);
75
73
  continue;
76
74
  }
77
75
 
@@ -87,7 +85,7 @@ async function waitForBuilder(
87
85
  currentEpoch,
88
86
  slot: clock.getCurrentSlot(),
89
87
  });
90
- await sleep(WAITING_FOR_BUILDER_POLL_MS, signal);
88
+ await sleep(msToNextSlotPoll(clock), signal);
91
89
  continue;
92
90
  }
93
91
  throw e;
@@ -101,10 +99,11 @@ async function waitForBuilder(
101
99
  }
102
100
  if (builder?.status === "pending") {
103
101
  logger.info("Waiting for builder deposit to be finalized", {id, slot: clock.getCurrentSlot()});
102
+ await sleep(msToNextEpochPoll(clock), signal);
104
103
  } else {
105
104
  logger.info("Waiting for builder to be known to the beacon node", {id, slot: clock.getCurrentSlot()});
105
+ await sleep(msToNextSlotPoll(clock), signal);
106
106
  }
107
- await sleep(WAITING_FOR_BUILDER_POLL_MS, signal);
108
107
  }
109
108
  throw new ErrorAborted("waitForBuilder");
110
109
  }
@@ -145,3 +144,11 @@ async function fetchBuilder(
145
144
  throw e;
146
145
  }
147
146
  }
147
+
148
+ function msToNextEpochPoll(clock: IClock): number {
149
+ return clock.msToSlot(computeStartSlotAtEpoch(clock.getCurrentEpoch() + 1));
150
+ }
151
+
152
+ function msToNextSlotPoll(clock: IClock): number {
153
+ return clock.msToSlot(clock.getCurrentSlot() + 1);
154
+ }
@@ -0,0 +1,245 @@
1
+ import {SLOTS_PER_EPOCH} from "@lodestar/params";
2
+ import type {Epoch, RootHex, Slot} from "@lodestar/types";
3
+ import {LodestarError} from "@lodestar/utils";
4
+
5
+ export type SubmittedBid = {
6
+ slot: Slot;
7
+ parentBlockHash: RootHex;
8
+ parentBlockRoot: RootHex;
9
+ blockHash: RootHex;
10
+ valueGwei: number;
11
+ signedBidRoot: RootHex;
12
+ };
13
+
14
+ export type BidIdentity = Pick<SubmittedBid, "slot" | "parentBlockHash" | "parentBlockRoot" | "blockHash">;
15
+
16
+ export type BidLedgerRecord = SubmittedBid & {
17
+ wonBlockRoots: RootHex[];
18
+ };
19
+
20
+ type RevealedPayload = {
21
+ slot: Slot;
22
+ blockHash: RootHex;
23
+ envelopeRoot: RootHex;
24
+ published: boolean;
25
+ };
26
+
27
+ type MutableBidLedgerRecord = SubmittedBid & {
28
+ wonBlockRoots: Set<RootHex>;
29
+ paymentSettled: boolean;
30
+ };
31
+
32
+ export enum BidLedgerErrorCode {
33
+ INVALID_BID_VALUE = "BID_LEDGER_ERROR_INVALID_BID_VALUE",
34
+ DUPLICATE_BID = "BID_LEDGER_ERROR_DUPLICATE_BID",
35
+ REVEAL_CONFLICT = "BID_LEDGER_ERROR_REVEAL_CONFLICT",
36
+ UNSETTLED_VALUE_OVERFLOW = "BID_LEDGER_ERROR_UNSETTLED_VALUE_OVERFLOW",
37
+ }
38
+
39
+ export type BidLedgerErrorType =
40
+ | {
41
+ code: BidLedgerErrorCode.INVALID_BID_VALUE;
42
+ valueGwei: number;
43
+ }
44
+ | {
45
+ code: BidLedgerErrorCode.DUPLICATE_BID;
46
+ slot: Slot;
47
+ parentBlockHash: RootHex;
48
+ parentBlockRoot: RootHex;
49
+ }
50
+ | {
51
+ code: BidLedgerErrorCode.REVEAL_CONFLICT;
52
+ blockRoot: RootHex;
53
+ blockHash: RootHex;
54
+ revealedBlockHash: RootHex;
55
+ envelopeRoot: RootHex;
56
+ revealedEnvelopeRoot: RootHex;
57
+ }
58
+ | {
59
+ code: BidLedgerErrorCode.UNSETTLED_VALUE_OVERFLOW;
60
+ currentEpoch: Epoch;
61
+ };
62
+
63
+ export class BidLedgerError extends LodestarError<BidLedgerErrorType> {}
64
+
65
+ const RECORD_RETENTION_EPOCHS = 3;
66
+ const KEEP_SLOTS = RECORD_RETENTION_EPOCHS * SLOTS_PER_EPOCH;
67
+
68
+ /** Tracks at most one bid per (slot, parentBlockHash, parentBlockRoot) and its reveal obligations. */
69
+ export class BidLedger {
70
+ private readonly bidsBySlot = new Map<Slot, Map<string, MutableBidLedgerRecord>>();
71
+ private readonly revealedPayloadByBlockRoot = new Map<RootHex, RevealedPayload>();
72
+
73
+ hasSubmitted(slot: Slot, parentBlockHash: RootHex, parentBlockRoot: RootHex): boolean {
74
+ return this.bidsBySlot.get(slot)?.has(tupleKey(parentBlockHash, parentBlockRoot)) ?? false;
75
+ }
76
+
77
+ recordBid(bid: SubmittedBid): BidLedgerRecord {
78
+ if (!Number.isSafeInteger(bid.valueGwei) || bid.valueGwei < 0) {
79
+ throw new BidLedgerError(
80
+ {code: BidLedgerErrorCode.INVALID_BID_VALUE, valueGwei: bid.valueGwei},
81
+ `Invalid bid value valueGwei=${bid.valueGwei}`
82
+ );
83
+ }
84
+
85
+ let bidsForSlot = this.bidsBySlot.get(bid.slot);
86
+ if (bidsForSlot === undefined) {
87
+ bidsForSlot = new Map();
88
+ this.bidsBySlot.set(bid.slot, bidsForSlot);
89
+ }
90
+
91
+ const key = tupleKey(bid.parentBlockHash, bid.parentBlockRoot);
92
+ if (bidsForSlot.has(key)) {
93
+ throw new BidLedgerError(
94
+ {
95
+ code: BidLedgerErrorCode.DUPLICATE_BID,
96
+ slot: bid.slot,
97
+ parentBlockHash: bid.parentBlockHash,
98
+ parentBlockRoot: bid.parentBlockRoot,
99
+ },
100
+ `Bid already recorded slot=${bid.slot} parentBlockHash=${bid.parentBlockHash} parentBlockRoot=${bid.parentBlockRoot}`
101
+ );
102
+ }
103
+
104
+ const record = {...bid, wonBlockRoots: new Set<RootHex>(), paymentSettled: false};
105
+ bidsForSlot.set(key, record);
106
+ return toRecord(record);
107
+ }
108
+
109
+ recordWin(identity: BidIdentity & Pick<SubmittedBid, "signedBidRoot">, blockRoot: RootHex): BidLedgerRecord | null {
110
+ const record = this.getBid(identity.slot, identity.parentBlockHash, identity.parentBlockRoot);
111
+ if (record === null || record.blockHash !== identity.blockHash || record.signedBidRoot !== identity.signedBidRoot) {
112
+ return null;
113
+ }
114
+
115
+ record.wonBlockRoots.add(blockRoot);
116
+ return toRecord(record);
117
+ }
118
+
119
+ /** Release a winning bid liability only after its payment is known to be reflected in Builder balance. */
120
+ recordPaymentSettled(identity: BidIdentity): BidLedgerRecord | null {
121
+ const record = this.getBid(identity.slot, identity.parentBlockHash, identity.parentBlockRoot);
122
+ if (record === null || record.blockHash !== identity.blockHash || record.wonBlockRoots.size === 0) {
123
+ return null;
124
+ }
125
+
126
+ record.paymentSettled = true;
127
+ return toRecord(record);
128
+ }
129
+
130
+ canReveal(blockRoot: RootHex, blockHash: RootHex, envelopeRoot: RootHex): boolean {
131
+ const revealedPayload = this.revealedPayloadByBlockRoot.get(blockRoot);
132
+ return (
133
+ revealedPayload === undefined ||
134
+ (revealedPayload.blockHash === blockHash && revealedPayload.envelopeRoot === envelopeRoot)
135
+ );
136
+ }
137
+
138
+ hasRevealed(blockRoot: RootHex): boolean {
139
+ return this.revealedPayloadByBlockRoot.has(blockRoot);
140
+ }
141
+
142
+ hasPublishedReveal(blockRoot: RootHex): boolean {
143
+ return this.revealedPayloadByBlockRoot.get(blockRoot)?.published ?? false;
144
+ }
145
+
146
+ recordReveal(slot: Slot, blockRoot: RootHex, blockHash: RootHex, envelopeRoot: RootHex): void {
147
+ const revealedPayload = this.revealedPayloadByBlockRoot.get(blockRoot);
148
+ if (revealedPayload !== undefined) {
149
+ if (revealedPayload.blockHash !== blockHash || revealedPayload.envelopeRoot !== envelopeRoot) {
150
+ throw new BidLedgerError(
151
+ {
152
+ code: BidLedgerErrorCode.REVEAL_CONFLICT,
153
+ blockRoot,
154
+ blockHash,
155
+ revealedBlockHash: revealedPayload.blockHash,
156
+ envelopeRoot,
157
+ revealedEnvelopeRoot: revealedPayload.envelopeRoot,
158
+ },
159
+ `Different envelope already recorded blockRoot=${blockRoot} envelopeRoot=${revealedPayload.envelopeRoot}`
160
+ );
161
+ }
162
+ return;
163
+ }
164
+
165
+ this.revealedPayloadByBlockRoot.set(blockRoot, {slot, blockHash, envelopeRoot, published: false});
166
+ }
167
+
168
+ recordRevealPublished(slot: Slot, blockRoot: RootHex, blockHash: RootHex, envelopeRoot: RootHex): void {
169
+ this.recordReveal(slot, blockRoot, blockHash, envelopeRoot);
170
+ const revealedPayload = this.revealedPayloadByBlockRoot.get(blockRoot);
171
+ if (revealedPayload !== undefined) {
172
+ revealedPayload.published = true;
173
+ }
174
+ }
175
+
176
+ getUnsettledValueGwei(currentEpoch: Epoch): number {
177
+ let total = 0;
178
+ for (const bidsForSlot of this.bidsBySlot.values()) {
179
+ for (const record of bidsForSlot.values()) {
180
+ if (record.wonBlockRoots.size === 0 || record.paymentSettled) {
181
+ continue;
182
+ }
183
+
184
+ total += record.valueGwei;
185
+ if (!Number.isSafeInteger(total)) {
186
+ throw new BidLedgerError(
187
+ {code: BidLedgerErrorCode.UNSETTLED_VALUE_OVERFLOW, currentEpoch},
188
+ `Unsettled bid value exceeds the safe integer range currentEpoch=${currentEpoch}`
189
+ );
190
+ }
191
+ }
192
+ }
193
+ return total;
194
+ }
195
+
196
+ getBidsForSlot(slot: Slot): BidLedgerRecord[] {
197
+ return Array.from(this.bidsBySlot.get(slot)?.values() ?? [], toRecord);
198
+ }
199
+
200
+ prune(currentSlot: Slot): number {
201
+ let removed = 0;
202
+ for (const [slot, bidsForSlot] of this.bidsBySlot) {
203
+ if (slot >= currentSlot - KEEP_SLOTS) {
204
+ continue;
205
+ }
206
+
207
+ for (const [key, record] of bidsForSlot) {
208
+ if (record.wonBlockRoots.size === 0 || record.paymentSettled) {
209
+ bidsForSlot.delete(key);
210
+ removed++;
211
+ }
212
+ }
213
+ if (bidsForSlot.size === 0) {
214
+ this.bidsBySlot.delete(slot);
215
+ }
216
+ }
217
+
218
+ for (const [blockRoot, revealedPayload] of this.revealedPayloadByBlockRoot) {
219
+ if (revealedPayload.slot < currentSlot - KEEP_SLOTS) {
220
+ this.revealedPayloadByBlockRoot.delete(blockRoot);
221
+ }
222
+ }
223
+ return removed;
224
+ }
225
+
226
+ private getBid(slot: Slot, parentBlockHash: RootHex, parentBlockRoot: RootHex): MutableBidLedgerRecord | null {
227
+ return this.bidsBySlot.get(slot)?.get(tupleKey(parentBlockHash, parentBlockRoot)) ?? null;
228
+ }
229
+ }
230
+
231
+ function tupleKey(parentBlockHash: RootHex, parentBlockRoot: RootHex): string {
232
+ return `${parentBlockHash}:${parentBlockRoot}`;
233
+ }
234
+
235
+ function toRecord(record: MutableBidLedgerRecord): BidLedgerRecord {
236
+ return {
237
+ slot: record.slot,
238
+ parentBlockHash: record.parentBlockHash,
239
+ parentBlockRoot: record.parentBlockRoot,
240
+ blockHash: record.blockHash,
241
+ valueGwei: record.valueGwei,
242
+ signedBidRoot: record.signedBidRoot,
243
+ wonBlockRoots: Array.from(record.wonBlockRoots),
244
+ };
245
+ }