diff --git a/bolt-sidecar/src/json_rpc/api.rs b/bolt-sidecar/src/json_rpc/api.rs index cb3efce3..e7da262f 100644 --- a/bolt-sidecar/src/json_rpc/api.rs +++ b/bolt-sidecar/src/json_rpc/api.rs @@ -153,6 +153,9 @@ impl CommitmentsRpc for JsonRpcApi { // TODO: check if there is enough time left in the current slot + // Web demo: push an event to the demo server to notify the frontend + emit_bolt_demo_event("commitment request accepted"); + // Forward the constraints to mev-boost's builder API self.mevboost_client .post_constraints(&signed_constraints) @@ -162,6 +165,25 @@ impl CommitmentsRpc for JsonRpcApi { } } +fn emit_bolt_demo_event>(message: T) { + let msg = message.into(); + tokio::spawn(async move { + let client = reqwest::Client::new(); + client + .post("http://host.docker.internal:3001/events") + .header("Content-Type", "application/json") + .body( + serde_json::to_string( + &serde_json::json!({"message": format!("BOLT-SIDECAR: {}", msg)}), + ) + .unwrap(), + ) + .send() + .await + .expect("failed to send event to demo server"); + }); +} + #[cfg(test)] mod tests { use serde_json::Value; diff --git a/bolt-web-demo/backend/src/server.ts b/bolt-web-demo/backend/src/server.ts index 3d459e63..8cdd24b1 100644 --- a/bolt-web-demo/backend/src/server.ts +++ b/bolt-web-demo/backend/src/server.ts @@ -55,7 +55,18 @@ app.get("/retry-port-events", (req, res) => { }); app.get("/latest-slot", (req, res) => { - res.send({ slot: LATEST_SLOT }); + if (!DEVNET_ENDPOINTS[EventType.BEACON_CLIENT_URL_FOUND]) { + res.status(500).send({ message: "No beacon client URL found" }); + return; + } + + getSlot(DEVNET_ENDPOINTS[EventType.BEACON_CLIENT_URL_FOUND]).then((slot) => { + if (slot !== undefined) { + res.send({ slot }); + } else { + res.status(500).send("Could not fetch the latest slot"); + } + }); }); // Endpoint to send a signed preconfirmation transaction to the BOLT MEV sidecar @@ -70,28 +81,20 @@ app.post("/preconfirmation", async (req, res) => { return; } - const { signedTx, txHash } = req.body; - if (!signedTx || !txHash) { - res.status(400).send("No signedTx or txHash provided"); + const { slot, tx, signature } = req.body; + if (!tx || !signature) { + res.status(400).send("No tx or signature provided"); return; } - const slot = await getSlot(beaconClientUrl); - const preconfirmationResponse = await fetch(`http://${mevSidecarUrl}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: "1", jsonrpc: "2.0", - method: "eth_requestPreconfirmation", - params: [ - { - slot: slot + 2, - txHash, - rawTx: signedTx, - }, - ], + method: "bolt_inclusionPreconfirmation", + params: [{ slot, tx, signature }], }), }).then((response) => response.json()); @@ -106,12 +109,12 @@ server.listen(SERVER_PORT, () => { async function sendDevnetEvents() { waitForPort( ["cl-1-lighthouse-geth", "http"], - EventType.BEACON_CLIENT_URL_FOUND, + EventType.BEACON_CLIENT_URL_FOUND ); waitForPort( ["el-1-geth-lighthouse", "rpc"], - EventType.JSONRPC_PROVIDER_URL_FOUND, + EventType.JSONRPC_PROVIDER_URL_FOUND ); waitForPort(["mev-sidecar-api", "api"], EventType.MEV_SIDECAR_URL_FOUND); diff --git a/bolt-web-demo/frontend/public/bolt-logo.png b/bolt-web-demo/frontend/public/bolt-logo.png new file mode 100644 index 00000000..e6e366c2 Binary files /dev/null and b/bolt-web-demo/frontend/public/bolt-logo.png differ diff --git a/bolt-web-demo/frontend/src/app/favicon.ico b/bolt-web-demo/frontend/src/app/favicon.ico index 718d6fea..25c3c8a1 100644 Binary files a/bolt-web-demo/frontend/src/app/favicon.ico and b/bolt-web-demo/frontend/src/app/favicon.ico differ diff --git a/bolt-web-demo/frontend/src/app/page.tsx b/bolt-web-demo/frontend/src/app/page.tsx index 1f573d85..79277d70 100644 --- a/bolt-web-demo/frontend/src/app/page.tsx +++ b/bolt-web-demo/frontend/src/app/page.tsx @@ -1,19 +1,30 @@ "use client"; import io from "socket.io-client"; -import { useState, useEffect, useCallback } from "react"; +import Image from "next/image"; +import { useState, useEffect } from "react"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Button } from "@/components/ui/button"; -import { createAndSignTransaction } from "@/lib/wallet"; +import { createPreconfPayload } from "@/lib/wallet"; import { EventType } from "@/lib/types"; import { Progress } from "@/components/ui/progress"; -type Event = { message: string; type?: EventType; timestamp: string }; +type Event = { + message: string; + type?: EventType; + timestamp: string; + link?: string; +}; + +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); @@ -22,8 +33,6 @@ export default function Home() { const [providerUrl, setProviderUrl] = useState(""); const [explorerUrl, setExplorerUrl] = useState(""); - const SERVER_URL = "http://localhost:3001"; - useEffect(() => { fetch(`${SERVER_URL}/retry-port-events`); fetch(`${SERVER_URL}/latest-slot`) @@ -35,19 +44,23 @@ export default function Home() { const newSocket = io(SERVER_URL, { autoConnect: true }); newSocket.on("new-event", (event: Event) => { - console.log("Event from server:", event); + console.debug("Event from server:", event); // If the event has a special type, handle it differently switch (event.type) { case EventType.BEACON_CLIENT_URL_FOUND: + console.info("Beacon client URL found:", event.message); setBeaconClientUrl(event.message); return; case EventType.JSONRPC_PROVIDER_URL_FOUND: + console.info("Provider URL found:", event.message); setProviderUrl(event.message); return; case EventType.EXPLORER_URL_FOUND: + console.info("Explorer URL found:", event.message); setExplorerUrl(event.message); case EventType.MEV_SIDECAR_URL_FOUND: + console.info("MEV sidecar URL found:", event.message); return; case EventType.NEW_SLOT: setNewSlotNumber(Number(event.message)); @@ -56,36 +69,28 @@ export default function Home() { break; } + setEvents((prev) => [event, ...prev]); + + // If the event is a preconfirmation, extract the tx hash and slot number + // and display a message with the explorer URL if ( event.message .toLowerCase() - .includes("preconfirmation proof verified for tx hash") + .includes("verified merkle proof for tx_hash") ) { - const txHash = event.message.match(/0x[a-fA-F0-9]{64}/g); - const slot = event.message - .match(/slot \d+/g) - ?.toString() - .match(/\d+/g) - ?.toString(); - - new Promise((_) => - setTimeout(() => { - const event: Event = { - message: `Preconfirmation ${txHash} available here: ${explorerUrl}/slot/${slot}`, - timestamp: new Date().toISOString(), - }; - setEvents((prev) => [event, ...prev]); - }, 1000), - ); + setPreconfIncluded(true); + dispatchEvent({ + message: `Preconfirmation included`, + link: `${explorerUrl}/slot/${preconfSlot}`, + timestamp: new Date().toISOString(), + }); } - - setEvents((prev) => [event, ...prev]); }); return () => { newSocket.close(); }; - }, [explorerUrl]); + }, [explorerUrl, preconfSlot]); useEffect(() => { let interval: any = null; @@ -101,38 +106,46 @@ export default function Home() { return () => clearInterval(interval); }, [timerActive]); - const sendPreconfirmation = useCallback( - async function () { - setEvents([]); - setPreconfSent(true); - try { - const { signedTx, txHash } = - await createAndSignTransaction(providerUrl); - - // 1. POST preconfirmation. - // The preconfirmation is considered valid as soon as the server responds with a 200 status code. - setTime(0); - setTimerActive(true); - const res = await fetch("http://localhost:3001/preconfirmation", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ signedTx, txHash }), - }); - if (res.status === 200) { - console.log("Preconfirmation successful"); - setTimerActive(false); - } - } catch (e) { - console.error(e); + async function sendPreconfirmation() { + // Reset state + setEvents([]); + setPreconfSent(true); + setPreconfIncluded(false); + + try { + const { payload, txHash } = await createPreconfPayload(providerUrl); + setPreconfSlot(payload.slot); + dispatchEvent({ + message: `Preconfirmation request sent for tx: ${txHash} at slot ${payload.slot}`, + timestamp: new Date().toISOString(), + }); + + // 1. POST preconfirmation. + // The preconfirmation is considered valid as soon as the server responds with a 200 status code. + setTime(0); + setTimerActive(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); } - }, - [providerUrl], - ); + } catch (e) { + console.error(e); + } + } + + function dispatchEvent(event: Event) { + setEvents((prev) => [event, ...prev]); + } return (
-

BOLT

+ BOLT

Your friendly preconfirmation companion.

@@ -140,11 +153,11 @@ export default function Home() { {newSlotNumber < 128 ? ( <> {newSlotNumber === -1 ? ( -
+

Loading...

) : ( -
+

MEV Boost is not active yet, please wait @@ -212,7 +225,18 @@ export default function Home() {

  • {parseDateToMs(message.timestamp)} {" | "} - {JSON.stringify(message.message)} + {message.message.toString()} + {message.link && ( + + {" "} + [link] + + )}
  • ))} diff --git a/bolt-web-demo/frontend/src/lib/wallet.ts b/bolt-web-demo/frontend/src/lib/wallet.ts index c95abacc..4a7a63a8 100644 --- a/bolt-web-demo/frontend/src/lib/wallet.ts +++ b/bolt-web-demo/frontend/src/lib/wallet.ts @@ -1,3 +1,4 @@ +import { SERVER_URL } from "@/app/page"; import { TransactionRequest, keccak256 } from "ethers"; import { ethers } from "ethers"; @@ -5,10 +6,15 @@ import { ethers } from "ethers"; const PRIVATE_KEY = "39725efee3fb28614de3bacaffe4cc4bd8c436257e2c8bb887c4b5c4be45e76d"; -export async function createAndSignTransaction(providerUrl: string): Promise<{ - signedTx: string; - txHash: string; -}> { +type InclusionRequestPayload = { + slot: number; + tx: string; + signature: string; +}; + +export async function createPreconfPayload( + providerUrl: string +): Promise<{ payload: InclusionRequestPayload; txHash: string }> { // Create a Wallet instance from a private key const provider = new ethers.JsonRpcProvider(providerUrl); const wallet = new ethers.Wallet(PRIVATE_KEY, provider); @@ -20,9 +26,9 @@ export async function createAndSignTransaction(providerUrl: string): Promise<{ from: await wallet.getAddress(), to: "0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD", value: ethers.parseEther("0.0069420"), - maxFeePerGas: ethers.parseUnits("20", "gwei"), - maxPriorityFeePerGas: ethers.parseUnits("3", "gwei"), - data: "0x", + maxFeePerGas: ethers.parseUnits("200", "gwei"), + maxPriorityFeePerGas: ethers.parseUnits("30", "gwei"), + data: "0xdeadbeef", }; const estimatedGas = await wallet.estimateGas(tx); @@ -31,8 +37,45 @@ export async function createAndSignTransaction(providerUrl: string): Promise<{ const populated = await wallet.populateCall(tx); const signedTx = await wallet.signTransaction(populated); const txHash = keccak256(signedTx); + const slot = (await getLatestSlot()) + 2; + + console.log("preconf target slot: ", slot); + + // Create a signature over the request fields "slot" and "tx" using the same signer + // to authenticate the preconfirmation request through bolt. + const slotBytes = numberToLittleEndianBytes(slot); + const txHashBytes = hexToBytes(txHash); + const message = new Uint8Array(slotBytes.length + txHashBytes.length); + message.set(slotBytes); + message.set(txHashBytes, slotBytes.length); - console.log({ signedTx, txHash }); + const messageDigest = keccak256(message); + const signature = wallet.signingKey.sign(messageDigest).serialized; + + return { payload: { slot, tx: signedTx, signature }, txHash }; +} + +export async function getLatestSlot(): Promise { + const slotResponse = await fetch(`${SERVER_URL}/latest-slot`).then( + (response) => response.json() + ); + return Number(slotResponse.slot); +} + +// Function to convert a number to a little-endian byte array +function numberToLittleEndianBytes(num: number): Uint8Array { + const buffer = new ArrayBuffer(8); // Assuming slot_number is a 64-bit integer + const view = new DataView(buffer); + view.setUint32(0, num, true); // true for little-endian + return new Uint8Array(buffer); +} - return { signedTx, txHash }; +// Function to decode a hex string to a byte array +function hexToBytes(hex: string): Uint8Array { + hex = hex.replace(/^0x/, ""); // Remove "0x" prefix if present + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substr(i, 2), 16); + } + return bytes; } diff --git a/builder/builder/builder.go b/builder/builder/builder.go index 1fd54737..3af7b9c5 100644 --- a/builder/builder/builder.go +++ b/builder/builder/builder.go @@ -528,16 +528,9 @@ func (b *Builder) onSealedBlock(opts SubmitBlockOpts, constraints types.HashToCo } timeForProofs := time.Since(timeStart) - event := strings.NewReader( - fmt.Sprintf("{ \"message\": \"BOLT-BUILDER: Created %d merkle proofs for block %d in %v\"}", - len(constraints), opts.Block.Number(), timeForProofs)) - eventRes, err := http.Post("http://host.docker.internal:3001/events", "application/json", event) - if err != nil { - log.Error("Failed to log preconfirms event: ", err) - } - if eventRes != nil { - defer eventRes.Body.Close() - } + // 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) versionedBlockRequestWithPreconfsProofs = &common.VersionedSubmitBlockRequestWithProofs{ Inner: versionedBlockRequest, diff --git a/builder/builder/relay.go b/builder/builder/relay.go index 24e44b0e..07be6ffb 100644 --- a/builder/builder/relay.go +++ b/builder/builder/relay.go @@ -200,15 +200,8 @@ func (r *RemoteRelay) SubmitBlockWithProofs(msg *common.VersionedSubmitBlockRequ // BOLT: send event to web demo if len(msg.Proofs) > 0 { slot, _ := msg.Inner.Slot() - event := strings.NewReader( - fmt.Sprintf("{ \"message\": \"BOLT-BUILDER: sending bid to relay with %d preconfirmations for slot %d\"}", len(msg.Proofs), slot)) - eventRes, err := http.Post("http://host.docker.internal:3001/events", "application/json", event) - if err != nil { - log.Error("Failed to log preconfirms event: ", err) - } - if eventRes != nil { - defer eventRes.Body.Close() - } + message := fmt.Sprintf("sending bid to relay with %d constraints for slot %d", len(msg.Proofs), slot) + EmitBoltDemoEvent(message) } switch msg.Inner.Version { diff --git a/builder/builder/utils.go b/builder/builder/utils.go index b6f3836b..a6c6cb06 100644 --- a/builder/builder/utils.go +++ b/builder/builder/utils.go @@ -9,9 +9,11 @@ import ( "fmt" "io" "net/http" + "strings" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" ) var errHTTPErrorResponse = errors.New("HTTP error response") @@ -132,3 +134,16 @@ func SendHTTPRequest(ctx context.Context, client http.Client, method, url string return resp.StatusCode, nil } + +// EmitBoltDemoEvent sends a message to the web demo backend to log an event. +// This is only used for demo purposes and should be removed in production. +func EmitBoltDemoEvent(message string) { + event := strings.NewReader(fmt.Sprintf("{ \"message\": \"BOLT-BUILDER: %s\"}", message)) + eventRes, err := http.Post("http://host.docker.internal:3001/events", "application/json", event) + if err != nil { + log.Error("Failed to send web demo event: ", err) + } + if eventRes != nil { + defer eventRes.Body.Close() + } +} diff --git a/mev-boost-relay/services/api/service.go b/mev-boost-relay/services/api/service.go index 0611a068..727c252f 100644 --- a/mev-boost-relay/services/api/service.go +++ b/mev-boost-relay/services/api/service.go @@ -2694,18 +2694,8 @@ func (api *RelayAPI) handleSubmitNewBlockWithProofs(w http.ResponseWriter, req * // BOLT: Send an event to the web demo slot, _ := payload.Inner.Slot() - message := fmt.Sprintf("BOLT-RELAY: received block bid with %d preconfirmations for slot %d", len(payload.Proofs), slot) - api.log.Infof(message) - event := strings.NewReader(fmt.Sprintf("{ \"message\": \"%s\"}", message)) - eventRes, err := http.Post("http://host.docker.internal:3001/events", "application/json", event) - if err != nil { - log.Errorf("Failed to log preconfirms event: %s", err) - } - if eventRes != nil { - defer eventRes.Body.Close() - } - - api.boltLog.Infof("Headslot: %d\n", headSlot) + message := fmt.Sprintf("received block bid with %d preconfirmations for slot %d", len(payload.Proofs), slot) + EmitBoltDemoEvent(message) nextTime = time.Now().UTC() pf.Decode = uint64(nextTime.Sub(prevTime).Microseconds()) diff --git a/mev-boost-relay/services/api/utils.go b/mev-boost-relay/services/api/utils.go index 4f92d6fc..fe0f37cf 100644 --- a/mev-boost-relay/services/api/utils.go +++ b/mev-boost-relay/services/api/utils.go @@ -3,6 +3,7 @@ package api import ( "encoding/json" "fmt" + "net/http" "strings" builderApi "github.com/attestantio/go-builder-client/api" @@ -221,3 +222,16 @@ func Find[T any](slice []*T, predicate func(arg *T) bool) *T { } return nil } + +// EmitBoltDemoEvent sends a message to the web demo backend to log an event. +// This is only used for demo purposes and should be removed in production. +func EmitBoltDemoEvent(message string) { + event := strings.NewReader(fmt.Sprintf("{ \"message\": \"BOLT-RELAY: %s\"}", message)) + eventRes, err := http.Post("http://host.docker.internal:3001/events", "application/json", event) + if err != nil { + fmt.Printf("Failed to send web demo event: %v", err) + } + if eventRes != nil { + defer eventRes.Body.Close() + } +} diff --git a/mev-boost/server/service.go b/mev-boost/server/service.go index a82f5ecc..344cef58 100644 --- a/mev-boost/server/service.go +++ b/mev-boost/server/service.go @@ -418,9 +418,18 @@ func (m *BoostService) verifyConstraintProofs(responsePayload *BidWithInclusionP if !ok { log.Error("[BOLT]: proof verification failed: 'not ok' for tx hash: ", proof.TxHash.String()) + + // BOLT: send event to web demo + message := fmt.Sprintf("failed to verify merkle proof for tx_hash %s", proof.TxHash.String()) + EmitBoltDemoEvent(message) + return errInvalidProofs } else { - log.Info(fmt.Sprintf("[BOLT]: Preconfirmation proof verified for tx hash %s in %s", proof.TxHash.String(), elapsed)) + log.Info(fmt.Sprintf("[BOLT]: Merkle proof verified for tx hash %s in %s", proof.TxHash.String(), elapsed)) + + // BOLT: send event to web demo + message := fmt.Sprintf("verified merkle proof for tx_hash %s in %v", proof.TxHash.String(), elapsed) + EmitBoltDemoEvent(message) } } } diff --git a/mev-boost/server/utils.go b/mev-boost/server/utils.go index fb779d53..725ce13b 100644 --- a/mev-boost/server/utils.go +++ b/mev-boost/server/utils.go @@ -270,3 +270,16 @@ func getPayloadResponseIsEmpty(payload *builderApi.VersionedSubmitBlindedBlockRe } return false } + +// EmitBoltDemoEvent sends a message to the web demo backend to log an event. +// This is only used for demo purposes and should be removed in production. +func EmitBoltDemoEvent(message string) { + event := strings.NewReader(fmt.Sprintf("{ \"message\": \"BOLT-MEV-BOOST: %s\"}", message)) + eventRes, err := http.Post("http://host.docker.internal:3001/events", "application/json", event) + if err != nil { + fmt.Printf("Failed to send web demo event: %v", err) + } + if eventRes != nil { + defer eventRes.Body.Close() + } +} diff --git a/scripts/start-demo.sh b/scripts/start-demo.sh index 6f417795..46284375 100755 --- a/scripts/start-demo.sh +++ b/scripts/start-demo.sh @@ -28,5 +28,12 @@ for command in "${commands[@]}"; do eval "$command" & # Use eval to handle complex commands with CD and chaining done +# Open the browser +if [ "$(uname)" = "Darwin" ]; then + open "http://localhost:3000" +elif [ "$(expr substr $(uname -s) 1 5)" = "Linux" ]; then + xdg-open "http://localhost:3000" +fi + # Wait for all background processes to finish wait