diff --git a/bolt-web-demo/backend/src/server.ts b/bolt-web-demo/backend/src/server.ts index 8cdd24b13..a799f9aa8 100644 --- a/bolt-web-demo/backend/src/server.ts +++ b/bolt-web-demo/backend/src/server.ts @@ -29,16 +29,21 @@ app.post("/events", (req, res) => { const { message } = req.body; if (!message) { + console.error("No message provided"); res.status(400).send("No message provided"); } else { - // Remove time measurements from the message - const messageWithoutMeasurements = message.replace(/ in .+$/g, ""); - // Deduplicate events - if (EVENTS_SET.has(messageWithoutMeasurements)) { - res.status(200).send("OK"); - return; - } - EVENTS_SET.add(messageWithoutMeasurements); + // // Remove time measurements from the message + // const messageWithoutMeasurements = message.replace(/ in .+$/g, ""); + // // Deduplicate events + // if (EVENTS_SET.has(messageWithoutMeasurements)) { + // console.warn( + // "Duplicate event received, discarding:", + // messageWithoutMeasurements + // ); + // res.status(200).send("OK"); + // return; + // } + // EVENTS_SET.add(messageWithoutMeasurements); // Broadcast the message to all connected WebSocket clients io.emit("new-event", { message, timestamp: new Date().toISOString() }); @@ -129,7 +134,7 @@ async function sendDevnetEvents() { sendDevnetEvents(); })(); -// Poll for the slot number until we reach slot 128 +// Poll for the latest slot number (async () => { let beaconClientUrl = DEVNET_ENDPOINTS?.[EventType.BEACON_CLIENT_URL_FOUND]; @@ -138,23 +143,14 @@ async function sendDevnetEvents() { beaconClientUrl = DEVNET_ENDPOINTS?.[EventType.BEACON_CLIENT_URL_FOUND]; } - LATEST_SLOT = await getSlot(beaconClientUrl); - while (LATEST_SLOT <= 128) { + while (true) { LATEST_SLOT = await getSlot(beaconClientUrl); - await new Promise((resolve) => setTimeout(resolve, 1000)); - io.emit("new-event", { type: EventType.NEW_SLOT, message: LATEST_SLOT, timestamp: new Date().toISOString(), }); - } - if (LATEST_SLOT > 128) { - io.emit("new-event", { - type: EventType.NEW_SLOT, - message: 128, - timestamp: new Date().toISOString(), - }); + await new Promise((resolve) => setTimeout(resolve, 1000)); } })(); diff --git a/bolt-web-demo/frontend/src/app/page.tsx b/bolt-web-demo/frontend/src/app/page.tsx index 6c88f8cd5..3b1ff65e0 100644 --- a/bolt-web-demo/frontend/src/app/page.tsx +++ b/bolt-web-demo/frontend/src/app/page.tsx @@ -21,17 +21,22 @@ export const SERVER_URL = "http://localhost:3001"; export default function Home() { const [events, setEvents] = useState>([]); - const [preconfSent, setPreconfSent] = useState(false); - const [preconfSlot, setPreconfSlot] = useState(-1); - const [preconfIncluded, setPreconfIncluded] = useState(false); - - const [timerActive, setTimerActive] = useState(false); - const [time, setTime] = useState(0); - - const [newSlotNumber, setNewSlotNumber] = useState(-1); - const [beaconClientUrl, setBeaconClientUrl] = useState(""); - const [providerUrl, setProviderUrl] = useState(""); - const [explorerUrl, setExplorerUrl] = useState(""); + const [preconfSent, setPreconfSent] = useState(false); + const [preconfSlot, setPreconfSlot] = useState(-1); + const [preconfIncluded, setPreconfIncluded] = useState(false); + const [preconfFinalized, setPreconfFinalized] = useState(false); + + const [preconfTimerActive, setPreconfTimerActive] = useState(false); + const [preconfTime, setPreconfTime] = useState(0); + const [inclusionTimerActive, setInclusionTimerActive] = useState(false); + const [inclusionTime, setInclusionTime] = useState(0); + const [finalizationTimerActive, setFinalizationTimerActive] = useState(false); + const [finalizationTime, setFinalizationTime] = useState(0); + + const [newSlotNumber, setNewSlotNumber] = useState(-1); + const [beaconClientUrl, setBeaconClientUrl] = useState(""); + const [providerUrl, setProviderUrl] = useState(""); + const [explorerUrl, setExplorerUrl] = useState(""); useEffect(() => { fetch(`${SERVER_URL}/retry-port-events`); @@ -44,7 +49,19 @@ export default function Home() { const newSocket = io(SERVER_URL, { autoConnect: true }); newSocket.on("new-event", (event: Event) => { - console.debug("Event from server:", event); + console.info("Event from server:", event); + + if (event.type === EventType.NEW_SLOT) { + const slot = Number(event.message); + if (slot === preconfSlot + 64) { + setPreconfFinalized(true); + setFinalizationTimerActive(false); + dispatchEvent({ + message: `Preconfirmed transaction finalized at slot ${slot}`, + timestamp: new Date().toISOString(), + }); + } + } // If the event has a special type, handle it differently switch (event.type) { @@ -77,6 +94,7 @@ export default function Home() { event.message.toLowerCase().includes("verified merkle proof for tx") ) { setPreconfIncluded(true); + setInclusionTimerActive(false); dispatchEvent({ message: `Preconfirmed transaction included at slot ${preconfSlot}`, link: `${explorerUrl}/slot/${preconfSlot}`, @@ -93,22 +111,54 @@ export default function Home() { useEffect(() => { let interval: any = null; - if (timerActive) { + if (preconfTimerActive) { interval = setInterval(() => { - setTime((prev) => prev + 2); + setPreconfTime((prev) => prev + 2); }, 2); } else { clearInterval(interval); } return () => clearInterval(interval); - }, [timerActive]); + }, [preconfTimerActive]); + + useEffect(() => { + let interval: any = null; + + if (inclusionTimerActive) { + interval = setInterval(() => { + setInclusionTime((prev) => prev + 10); + }, 10); + } else { + clearInterval(interval); + } + + return () => clearInterval(interval); + }, [inclusionTimerActive]); + + useEffect(() => { + let interval: any = null; + + if (finalizationTimerActive) { + interval = setInterval(() => { + setFinalizationTime((prev) => prev + 30); + }, 30); + } else { + clearInterval(interval); + } + + return () => clearInterval(interval); + }, [finalizationTimerActive]); async function sendPreconfirmation() { // Reset state setEvents([]); setPreconfSent(true); setPreconfIncluded(false); + setPreconfFinalized(false); + setPreconfTime(0); + setInclusionTime(0); + setFinalizationTime(0); try { const { payload, txHash } = await createPreconfPayload(providerUrl); @@ -120,16 +170,18 @@ export default function Home() { // 1. POST preconfirmation. // The preconfirmation is considered valid as soon as the server responds with a 200 status code. - setTime(0); - setTimerActive(true); + setPreconfTimerActive(true); + setInclusionTimerActive(true); + setFinalizationTimerActive(true); + const res = await fetch(`${SERVER_URL}/preconfirmation`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (res.status === 200) { - console.log("Preconfirmation successful"); - setTimerActive(false); + console.log("Preconfirmation response was successful"); + setPreconfTimerActive(false); } } catch (e) { console.error(e); @@ -140,6 +192,11 @@ export default function Home() { setEvents((prev) => [event, ...prev]); } + const getStatusClass = (status: boolean) => { + const base = "h-4 w-4 border border-gray-800 rounded-full "; + return base + (status ? "bg-green-500" : "bg-yellow-500"); + }; + return (
@@ -193,8 +250,46 @@ export default function Home() {
{beaconClientUrl && providerUrl ? (
+ {preconfSent && ( +
+

Status

+
    +
  • + Transaction preconfirmed: + + {preconfTime}ms +
  • +
  • + + Transaction confirmed (included in a block): + + + {inclusionTime / 1000}s +
  • +
  • + + Transaction finalized (2 epochs after inclusion): + + + {finalizationTime / 1000}s +
  • +
+
+ )} +
-

Step 1: send a transactions eligible for pre-confirmation

+

+ Step 1: Send a transaction eligible for preconfirmation +

By clicking this button you will create a transaction and send it as a preconfirmation request to the BOLT sidecar of the @@ -214,52 +309,45 @@ export default function Home() {
{preconfSent && ( -
-

- Step 2: wait for proposers to issue the preconfirmation response -

- - The transaction will be processed by BOLT and you will - receive a preconfirmation for inclusion in the next block. - - -
-

- Waiting for preconfirmation. Time elapsed: {time}ms + <> +

+

+ Step 2: Wait for proposers to issue the preconfirmation + response

+ + The transaction will be processed by BOLT and you will + receive a preconfirmation for inclusion in the next block. +
-
- )} - -
-

Event logs

- - This is the list of events received from the server. - - -
    - {events.map((message, index) => ( -
  • - {parseDateToMs(message.timestamp)} - {" | "} - {message.message.toString()} - {message.link && ( - - {" "} - [link] - - )} -
  • - ))} -
-
-
+
+

Event logs

+ +
    + {[...events].reverse().map((message, index) => ( +
  • + {parseDateToMs(message.timestamp)} + {" | "} + {message.message.toString()} + {message.link && ( + + {" "} + [link] + + )} +
  • + ))} +
+
+
+ + )}
) : (
diff --git a/builder/builder/builder.go b/builder/builder/builder.go index 3af7b9c50..c77418bac 100644 --- a/builder/builder/builder.go +++ b/builder/builder/builder.go @@ -390,6 +390,8 @@ func (b *Builder) subscribeToRelayForConstraints(relayBaseEndpoint, authHeader s continue } + EmitBoltDemoEvent(fmt.Sprintf("Received constraint from relay for slot %d, stored in cache (path: %s)", constraint.Message.Slot, SubscribeConstraintsPath)) + // For every constraint, we need to check if it has already been seen for the associated slot slotConstraints, _ := b.constraintsCache.Get(constraint.Message.Slot) if len(slotConstraints) == 0 { @@ -405,7 +407,9 @@ func (b *Builder) subscribeToRelayForConstraints(relayBaseEndpoint, authHeader s // Update the slot constraints in the cache b.constraintsCache.Put(constraint.Message.Slot, slotConstraints) + } + } return nil @@ -456,6 +460,7 @@ func (b *Builder) onSealedBlock(opts SubmitBlockOpts, constraints types.HashToCo var versionedBlockRequestWithPreconfsProofs *common.VersionedSubmitBlockRequestWithProofs if len(constraints) > 0 { + EmitBoltDemoEvent(fmt.Sprintf("sealing block %d with %d constraints", opts.Block.Number(), len(constraints))) log.Info(fmt.Sprintf("[BOLT]: Sealing block with %d preconfirmed transactions for block %d", len(constraints), opts.Block.Number())) payloadTransactions := opts.Block.Transactions() @@ -529,8 +534,7 @@ func (b *Builder) onSealedBlock(opts SubmitBlockOpts, constraints types.HashToCo timeForProofs := time.Since(timeStart) // BOLT: send event to web demo - message := fmt.Sprintf("created %d merkle proofs for block %d in %v", len(constraints), opts.Block.Number(), timeForProofs) - EmitBoltDemoEvent(message) + EmitBoltDemoEvent(fmt.Sprintf("created %d merkle proofs for block %d in %v", len(constraints), opts.Block.Number(), timeForProofs)) versionedBlockRequestWithPreconfsProofs = &common.VersionedSubmitBlockRequestWithProofs{ Inner: versionedBlockRequest, diff --git a/builder/builder/relay.go b/builder/builder/relay.go index 07be6ffb1..f4d2f7df2 100644 --- a/builder/builder/relay.go +++ b/builder/builder/relay.go @@ -199,9 +199,8 @@ func (r *RemoteRelay) SubmitBlockWithProofs(msg *common.VersionedSubmitBlockRequ // BOLT: send event to web demo if len(msg.Proofs) > 0 { - slot, _ := msg.Inner.Slot() - message := fmt.Sprintf("sending bid to relay with %d constraints for slot %d", len(msg.Proofs), slot) - EmitBoltDemoEvent(message) + number, _ := msg.Inner.BlockNumber() + EmitBoltDemoEvent(fmt.Sprintf("sending block %d with proofs to relay (path: %s)", number, "/relay/v1/builder/blocks_with_proofs")) } switch msg.Inner.Version { diff --git a/mev-boost-relay/services/api/service.go b/mev-boost-relay/services/api/service.go index 727c252f2..3164f4b65 100644 --- a/mev-boost-relay/services/api/service.go +++ b/mev-boost-relay/services/api/service.go @@ -1870,6 +1870,8 @@ func (api *RelayAPI) handleSubmitConstraints(w http.ResponseWriter, req *http.Re log.Infof("Added %d constraints for slot %d and broadcasted %d to channels", len(*payload), message.Slot, len(api.constraintsConsumers)) } + EmitBoltDemoEvent(fmt.Sprintf("received %d valid constraints, sending to builders... (path: %s)", len(*payload), req.URL.Path)) + // respond to the HTTP request api.RespondOK(w, nil) } diff --git a/mev-boost/server/service.go b/mev-boost/server/service.go index 0ce504210..fdd18e118 100644 --- a/mev-boost/server/service.go +++ b/mev-boost/server/service.go @@ -446,6 +446,8 @@ func (m *BoostService) handleSubmitConstraint(w http.ResponseWriter, req *http.R "ua": ua, }) + path := req.URL.Path + log.Info("submitConstraint") payload := BatchedSignedConstraints{} @@ -474,6 +476,8 @@ func (m *BoostService) handleSubmitConstraint(w http.ResponseWriter, req *http.R relayRespCh := make(chan error, len(m.relays)) + EmitBoltDemoEvent(fmt.Sprintf("received %d constraints, forwarding to Bolt relays... (path: %s)", len(payload), path)) + for _, relay := range m.relays { go func(relay RelayEntry) { url := relay.GetURI(pathSubmitConstraint)