From 79b1d5d58c45c2c64b39ec136948cdb6dbc7f747 Mon Sep 17 00:00:00 2001 From: Pablo Maldonado Date: Wed, 11 Dec 2024 16:59:51 -0600 Subject: [PATCH] feat(api): relayer config auth (#1322) * wip * updates Signed-off-by: james-a-morris * feat(api): add relayer config enpoint Signed-off-by: Pablo Maldonado * feat(api): get relayer address and check whitelist Signed-off-by: Pablo Maldonado * refactor: comments Signed-off-by: Pablo Maldonado * Update api/relayer-config.ts Co-authored-by: James Morris, MS <96435344+james-a-morris@users.noreply.github.com> * refactor: organise files Signed-off-by: Pablo Maldonado * refactor: rename func Signed-off-by: Pablo Maldonado * feat: fixes and tests Signed-off-by: Pablo Maldonado * feat: remove query Signed-off-by: Pablo Maldonado * feat: types Signed-off-by: Pablo Maldonado * feat: add config types Signed-off-by: Pablo Maldonado * fix: types Signed-off-by: Pablo Maldonado * feat: simplify Signed-off-by: Pablo Maldonado * Update api/_types/exclusivity.types.ts Co-authored-by: James Morris, MS <96435344+james-a-morris@users.noreply.github.com> * Update api/relayer-config.ts Co-authored-by: James Morris, MS <96435344+james-a-morris@users.noreply.github.com> * feat: simplify handler Signed-off-by: Pablo Maldonado --------- Signed-off-by: james-a-morris Signed-off-by: Pablo Maldonado Co-authored-by: Paul <108695806+pxrl@users.noreply.github.com> Co-authored-by: james-a-morris Co-authored-by: James Morris, MS <96435344+james-a-morris@users.noreply.github.com> --- api/_exclusivity/utils.ts | 22 ++++++++ api/_types/exclusivity.types.ts | 11 ++++ api/_types/generic.types.ts | 5 +- api/relayer-config.ts | 38 +++++++++----- test/api/relayer-config.test.ts | 91 +++++++++++++++++++++++++++++++++ 5 files changed, 153 insertions(+), 14 deletions(-) create mode 100644 api/_exclusivity/utils.ts create mode 100644 test/api/relayer-config.test.ts diff --git a/api/_exclusivity/utils.ts b/api/_exclusivity/utils.ts new file mode 100644 index 000000000..bc6382572 --- /dev/null +++ b/api/_exclusivity/utils.ts @@ -0,0 +1,22 @@ +import { ethers } from "ethers"; + +export const MAX_MESSAGE_AGE_SECONDS = 300; + +// TODO: get this from gh +export const getWhiteListedRelayers = () => { + return [ + "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D", // dev wallet + ]; +}; + +export const getRelayerFromSignature = (signature: string, message: string) => { + return ethers.utils.verifyMessage(message, signature); +}; + +export const isTimestampValid = ( + timestamp: number, + maxAgeSeconds: number +): boolean => { + const currentTime = Math.floor(Date.now() / 1000); + return currentTime - timestamp <= maxAgeSeconds; +}; diff --git a/api/_types/exclusivity.types.ts b/api/_types/exclusivity.types.ts index c0288a6ac..17f1cba62 100644 --- a/api/_types/exclusivity.types.ts +++ b/api/_types/exclusivity.types.ts @@ -1,5 +1,6 @@ import { array, boolean, Infer, object, optional } from "superstruct"; import { positiveFloatStr, positiveIntStr, validAddress } from "../_utils"; +import { TypedVercelRequest } from "./generic.types"; export const RelayerFillLimitSchema = object({ originChainId: positiveIntStr(), @@ -18,6 +19,16 @@ export const RelayerFillLimitArraySchema = array(RelayerFillLimitSchema); export type RelayerFillLimit = Infer; +export type RelayerConfigUpdate = { + timestamp: number; + relayerFillLimits: RelayerFillLimit[]; +}; + +export type TypedRelayerConfigUpdateRequest = TypedVercelRequest< + never, + RelayerConfigUpdate +>; + // // Example config. // export const RelayerConfigUpdate: RelayerFillLimit[] = [ // { diff --git a/api/_types/generic.types.ts b/api/_types/generic.types.ts index d3f38cc98..a6b7773ee 100644 --- a/api/_types/generic.types.ts +++ b/api/_types/generic.types.ts @@ -1,5 +1,6 @@ import { VercelRequest } from "@vercel/node"; -export type TypedVercelRequest = VercelRequest & { - query: Partial; +export type TypedVercelRequest = VercelRequest & { + query: Partial; + body: Partial; }; diff --git a/api/relayer-config.ts b/api/relayer-config.ts index 1118c2ce3..9759a343b 100644 --- a/api/relayer-config.ts +++ b/api/relayer-config.ts @@ -1,21 +1,35 @@ import { VercelResponse } from "@vercel/node"; -import { TypedVercelRequest } from "./_types"; +import { + getRelayerFromSignature, + getWhiteListedRelayers, + isTimestampValid, + MAX_MESSAGE_AGE_SECONDS, +} from "./_exclusivity/utils"; +import { RelayerConfigUpdate, TypedRelayerConfigUpdateRequest } from "./_types"; const handler = async ( - request: TypedVercelRequest, + request: TypedRelayerConfigUpdateRequest, response: VercelResponse ) => { - if (request.method === "GET") { - // Handle GET request - response.status(200).json({ message: "GET request received" }); - } else if (request.method === "POST") { - // Handle POST request - response.status(200).json({ message: "POST request received" }); - } else { - // Method not allowed - response.setHeader("Allow", ["GET", "POST"]); - response.status(405).end(`Method ${request.method} Not Allowed`); + if (request.method !== "POST") { + return response.status(405).end(`Method ${request.method} Not Allowed`); } + const body = request.body as RelayerConfigUpdate; + const { authorization } = request.headers; + const { timestamp } = body; + if (!isTimestampValid(timestamp, MAX_MESSAGE_AGE_SECONDS)) { + return response.status(400).json({ message: "Message too old" }); + } + + if ( + !authorization || + !getWhiteListedRelayers().includes( + getRelayerFromSignature(authorization, JSON.stringify(body)) + ) + ) { + return response.status(401).json({ message: "Unauthorized" }); + } + return response.status(200).json({ message: "POST request received" }); }; export default handler; diff --git a/test/api/relayer-config.test.ts b/test/api/relayer-config.test.ts new file mode 100644 index 000000000..a8d6200da --- /dev/null +++ b/test/api/relayer-config.test.ts @@ -0,0 +1,91 @@ +import { VercelResponse } from "@vercel/node"; +import { ethers } from "ethers"; +import * as utils from "../../api/_exclusivity/utils"; +import { TypedRelayerConfigUpdateRequest } from "../../api/_types"; +import handler from "../../api/relayer-config"; +const { MAX_MESSAGE_AGE_SECONDS } = utils; + +const getMockedResponse = () => { + const response: any = {}; + response.status = jest.fn().mockReturnValue(response); + response.json = jest.fn(); + response.setHeader = jest.fn(); + return response as VercelResponse; +}; + +describe("Relayer Config API", () => { + let response: VercelResponse; + const whitelistedRelayer = ethers.Wallet.createRandom(); + const unauthorizedRelayer = ethers.Wallet.createRandom(); + + beforeEach(() => { + response = getMockedResponse(); + jest + .spyOn(utils, "getWhiteListedRelayers") + .mockReturnValue([whitelistedRelayer.address]); + }); + + test("POST request with valid timestamp", async () => { + const message = { + timestamp: Date.now() / 1000, + }; + const messageString = JSON.stringify(message); + const signature = await whitelistedRelayer.signMessage(messageString); + + const request = { + method: "POST", + headers: { + authorization: signature, + }, + body: message, + } as TypedRelayerConfigUpdateRequest; + + await handler(request, response); + + expect(response.status).toHaveBeenCalledWith(200); + }); + + test("POST request with invalid timestamp", async () => { + const message = { + timestamp: Date.now() / 1000 - MAX_MESSAGE_AGE_SECONDS - 1, + }; + const signature = await whitelistedRelayer.signMessage( + JSON.stringify(message) + ); + + const request = { + method: "POST", + headers: { + authorization: signature, + }, + body: message, + } as TypedRelayerConfigUpdateRequest; + + await handler(request, response); + + expect(response.status).toHaveBeenCalledWith(400); + expect(response.json).toHaveBeenCalledWith({ message: "Message too old" }); + }); + + test("POST request with invalid signature", async () => { + const message = { + timestamp: Date.now() / 1000, + }; + const signature = await unauthorizedRelayer.signMessage( + JSON.stringify(message) + ); + + const request = { + method: "POST", + headers: { + authorization: signature, + }, + body: message, + } as TypedRelayerConfigUpdateRequest; + + await handler(request, response); + + expect(response.status).toHaveBeenCalledWith(401); + expect(response.json).toHaveBeenCalledWith({ message: "Unauthorized" }); + }); +});