Skip to content

Commit

Permalink
Merge to master (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
md0x authored Jul 4, 2024
2 parents 12b039e + 6ad4715 commit d7ebab2
Show file tree
Hide file tree
Showing 10 changed files with 404 additions and 26 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ OVAL_ADDRESS=0x420 # (Only if not using OVAL_CONFIGS)
REFUND_ADDRESS=0x42069 # (Only if not using OVAL_CONFIGS) The refund address you want to send the OEV kickback to.
REFUND_PERCENT=0.5 # (Only if not using OVAL_CONFIGS) The percentage of the OEV kickback to send to the refund address.
SENDER_PRIVATE_KEY=<your_sender_private_key> # (Only if not using OVAL_CONFIGS) Private key of the actor authorized to call unlockLatestValue on the Oval.
GCKMS_CONFIG=<gckms_config_json> # JSON string that specifies the GCKMS configuration for retrieving unlocker keys. (Optional)
AUTH_PRIVATE_KEY=<your_auth_private_key> # Root private key for deriving searcher-specific keys for signing bundles.
PROVIDER_URL=<your_provider_url> # Ethereum mainnet/goerli RPC provider URL.
Expand All @@ -47,10 +48,12 @@ CHAIN_ID=<network_chain_id> # Chain ID of the Ethereum network

`OVAL_CONFIGS` is a JSON string that maps Oval contract addresses to their specific configurations. Each entry in this JSON object should have the following format:


```json
{
"<Oval_Contract_Address>": {
"unlockerKey": "<Unlocker_Private_Key>",
"unlockerKey": "<Unlocker_Private_Key>", // Optional: Use either this or gckmsKeyId, not both.
"gckmsKeyId": "<GCKMS_Key_ID>", // Optional: Use either this or unlockerKey, not both.
"refundAddress": "<Refund_Address>",
"refundPercent": <Refund_Percentage>
}
Expand All @@ -59,6 +62,7 @@ CHAIN_ID=<network_chain_id> # Chain ID of the Ethereum network

- `Oval_Contract_Address`: The Ethereum address of the Oval instance.
- `Unlocker_Private_Key`: The private key of the wallet permitted to unlock prices in the specified Oval contract.
- `GCKMS_Key_ID`: The GCKMS key ID of the wallet permitted to unlock prices in the specified Oval contract.
- `Refund_Address`: The Ethereum address where refunds will be sent.
- `Refund_Percentage`: The percentage of the OEV kickback to send to the refund address.

Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
"author": "",
"dependencies": {
"@flashbots/mev-share-client": "^0.7.12",
"@google-cloud/kms": "^4.5.0",
"@google-cloud/storage": "^7.11.2",
"@types/axios": "^0.14.0",
"@types/body-parser": "^1.19.4",
"@types/express": "^4.17.20",
"@types/morgan": "^1.9.7",
"@types/uuid": "^9.0.8",
"@uma/logger": "^1.1.0",
"axios": "^1.5.1",
"body-parser": "^1.20.2",
Expand All @@ -20,8 +23,7 @@
"nodemon": "^3.0.1",
"prettier": "^3.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.2.2",
"@types/uuid": "^9.0.8"
"typescript": "^5.2.2"
},
"devDependencies": {
"@types/eventsource": "^1.1.11",
Expand Down
11 changes: 9 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
FLASHBOTS_SIGNATURE_HEADER,
Logger,
OVAL_ADDRESSES_HEADER,
WalletManager,
Refund,
adjustRefundPercent,
createUnlockLatestValueBundle,
Expand All @@ -46,6 +47,10 @@ app.use((req, res, next) => {
const provider = getProvider();
const { ovalConfigs } = env;

// Initialize unlocker wallets for each Oval instance.
const keyManager = WalletManager.getInstance();
keyManager.initialize(ovalConfigs);

// Start restful API server to listen for root inbound post requests.
app.post("/", async (req, res, next) => {
try {
Expand Down Expand Up @@ -121,7 +126,8 @@ app.post("/", async (req, res, next) => {
},
];

return sendBundle(req, res, mevshare, targetBlock, body.id, bundle, refunds);
await sendBundle(req, res, mevshare, targetBlock, body.id, bundle, refunds);
return;
} else {
// If configured, simulate the original bundle to check if it reverts without the unlock.
if (env.passThroughNonReverting) {
Expand Down Expand Up @@ -176,7 +182,8 @@ app.post("/", async (req, res, next) => {
}

// Exit the function here to prevent the request from being forwarded to the FORWARD_URL.
return sendBundle(req, res, mevshare, targetBlock, body.id, bundle, refunds);
await sendBundle(req, res, mevshare, targetBlock, body.id, bundle, refunds);
return;
} else if (verifiedSignatureSearcherPkey && body.method == "eth_callBundle") {
if (!isEthCallBundleParams(body.params)) {
Logger.info(req.transactionId, "Received unsupported eth_callBundle request!", { body });
Expand Down
6 changes: 3 additions & 3 deletions src/lib/bundleUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Interface, Transaction, TransactionRequest, Wallet } from "ethers";
import express from "express";
import { FlashbotsBundleProvider } from "flashbots-ethers-v6-provider-bundle";
import { getBaseFee, getMaxBlockByChainId, getProvider, initWallets } from "./helpers";
import { WalletManager, getBaseFee, getMaxBlockByChainId, getProvider } from "./helpers";

import MevShareClient, { BundleParams } from "@flashbots/mev-share-client";
import { JSONRPCID, createJSONRPCSuccessResponse } from "json-rpc-2.0";
Expand Down Expand Up @@ -55,11 +55,11 @@ export const prepareUnlockTransaction = async (
simulate = true,
) => {
const provider = getProvider();
const unlockerWallets = initWallets(provider);
const unlockerWallet = WalletManager.getInstance().getWallet(ovalAddress, provider);
const [baseFee, network] = await Promise.all([getBaseFee(provider, req), provider.getNetwork()]);
const data = ovalInterface.encodeFunctionData("unlockLatestValue");
const { unlockTxHash, signedUnlockTx } = await createUnlockLatestValueTx(
unlockerWallets[ovalAddress],
unlockerWallet,
baseFee,
data,
network.chainId,
Expand Down
1 change: 1 addition & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const fallback = {
chainId: 1,
port: 3000,
gckmsConfig: '{"projectId":"keys-oval-8149", "locationId":"us-east1", "keyRingId":"keyring-oval", "cryptoKeyId":"", "ciphertextBucket":"bucket-keyring-oval", "ciphertextFilename":""}',
forwardUrl: "https://relay.flashbots.net",
refundPercent: "90",
builders: [
Expand Down
2 changes: 2 additions & 0 deletions src/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,15 @@ type EnvironmentVariables = {
passThroughNonReverting: boolean;
maxOvalHeaderAddresses: number;
flashbotsOrigin: string | undefined;
gckmsConfig: string;
chainIdBlockOffsets: {
[key: number]: number;
};
};

export const env: EnvironmentVariables = {
port: getInt(getEnvVar("PORT", fallback.port.toString())),
gckmsConfig: getEnvVar("GCKMS_CONFIG", fallback.gckmsConfig),
authKey: getPrivateKey(getEnvVar("AUTH_PRIVATE_KEY")),
chainId,
providerUrl: getEnvVar("PROVIDER_URL"),
Expand Down
48 changes: 48 additions & 0 deletions src/lib/gckms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import kms from "@google-cloud/kms";
import { Storage } from "@google-cloud/storage";

export interface KeyConfig {
projectId: string;
locationId: string;
keyRingId: string;
cryptoKeyId: string;
ciphertextBucket: string;
ciphertextFilename: string;
}

const { GCP_STORAGE_CONFIG } = process.env;

// Allows the environment to customize the config that's used to interact with google cloud storage.
// Relevant options can be found here: https://googleapis.dev/nodejs/storage/latest/global.html#StorageOptions.
// Specific fields of interest:
// - timeout: allows the env to set the timeout for all http requests.
// - retryOptions: object that allows the caller to specify how the library retries.
const storageConfig = GCP_STORAGE_CONFIG ? JSON.parse(GCP_STORAGE_CONFIG) : undefined;

// This function takes an array of GCKMS configs that are shaped as follows:
// {
// projectId: "project-name",
// locationId: "asia-east2",
// keyRingId: "Keyring_Test",
// cryptoKeyId: "keyname",
// ciphertextBucket: "cipher_bucket",
// ciphertextFilename: "ciphertext_fname.enc",
// }
//
// It returns a private key that can be used to send transactions.
export async function retrieveGckmsKey(gckmsConfig: KeyConfig): Promise<string> {

const storage = new Storage(storageConfig);
const keyMaterialBucket = storage.bucket(gckmsConfig.ciphertextBucket);
const ciphertextFile = keyMaterialBucket.file(gckmsConfig.ciphertextFilename);

const contentsBuffer = (await ciphertextFile.download())[0];
const ciphertext = contentsBuffer.toString("base64");

// Send the request to decrypt the downloaded file.
const client = new kms.KeyManagementServiceClient();
const name = client.cryptoKeyPath(gckmsConfig.projectId, gckmsConfig.locationId, gckmsConfig.keyRingId, gckmsConfig.cryptoKeyId);
const [result] = await client.decrypt({ name, ciphertext });
if (!(result.plaintext instanceof Uint8Array)) throw new Error("result.plaintext wrong type");
return "0x" + Buffer.from(result.plaintext).toString().trim();
}
62 changes: 47 additions & 15 deletions src/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,13 @@ import { flashbotsSupportedNetworks, supportedNetworks } from "./constants";
import { env } from "./env";
import { Logger } from "./logging";
import { OvalAddressConfigList, OvalConfig, OvalConfigs } from "./types";
import { retrieveGckmsKey } from "./gckms";

export function getProvider() {
const network = new Network(supportedNetworks[env.chainId], env.chainId);
return new JsonRpcProvider(env.providerUrl, network);
}

// Initialize unlocker wallets for each Oval instance.
export function initWallets(provider: JsonRpcProvider) {
return Object.entries(env.ovalConfigs).reduce(
(wallets, [address, config]) => {
wallets[address] = new Wallet(config.unlockerKey).connect(provider);
return wallets;
},
{} as Record<string, Wallet>,
);
}

export async function initClients(provider: JsonRpcProvider, searcherPublicKey: string) {
// Derive a private key from the searcher's public key and the unlocker's private key.
// This approach ensures that each searcher maintains an independent reputation within the Flashbots network,
Expand Down Expand Up @@ -186,10 +176,14 @@ function isOvalConfig(input: unknown): input is OvalConfig {
typeof input === "object" &&
input !== null &&
!Array.isArray(input) &&
"unlockerKey" in input &&
typeof input["unlockerKey"] === "string" &&
((!input["unlockerKey"].startsWith("0x") && isHexString("0x" + input["unlockerKey"], 32)) ||
isHexString(input["unlockerKey"], 32)) &&
(
("unlockerKey" in input && typeof input["unlockerKey"] === "string" &&
((!input["unlockerKey"].startsWith("0x") && isHexString("0x" + input["unlockerKey"], 32)) ||
isHexString(input["unlockerKey"], 32)) &&
!("gckmsKeyId" in input)) ||
("gckmsKeyId" in input && typeof input["gckmsKeyId"] === "string" &&
!("unlockerKey" in input))
) &&
"refundAddress" in input &&
typeof input["refundAddress"] === "string" &&
isAddress(input["refundAddress"]) &&
Expand Down Expand Up @@ -219,6 +213,7 @@ const normaliseOvalConfigs = (config: OvalConfigs): OvalConfigs => {
for (const [address, ovalConfig] of Object.entries(config)) {
normalised[getAddress(address)] = {
unlockerKey: ovalConfig.unlockerKey,
gckmsKeyId: ovalConfig.gckmsKeyId,
refundAddress: getAddress(ovalConfig.refundAddress),
refundPercent: ovalConfig.refundPercent,
};
Expand Down Expand Up @@ -315,3 +310,40 @@ export function getMaxBlockByChainId(chainId: number, targetBlock: number) {
// In mainnet this is always the targetBlock, but in Goerli we add 24 blocks to the targetBlock.
return targetBlock + env.chainIdBlockOffsets[chainId];
}
export class WalletManager {
private static instance: WalletManager;
private wallets: Record<string, Wallet> = {};

private constructor() { }

public static getInstance(): WalletManager {
if (!WalletManager.instance) {
WalletManager.instance = new WalletManager();
}
return WalletManager.instance;
}

public async initialize(ovalConfigs: OvalConfigs) {
// Oval Config addresses are already checksummed.
for (const [address, config] of Object.entries(ovalConfigs)) {
if (config.unlockerKey) {
this.wallets[address] = new Wallet(config.unlockerKey);
} else if (config.gckmsKeyId) {
const gckmsKey = await retrieveGckmsKey({
...JSON.parse(env.gckmsConfig),
cryptoKeyId: config.gckmsKeyId,
ciphertextFilename: `${config.gckmsKeyId}.enc`,
});
this.wallets[address] = new Wallet(gckmsKey);
}
}
}

public getWallet(address: string, provider: JsonRpcProvider): Wallet {
const checkSummedAddress = getAddress(address);
if (!this.wallets[checkSummedAddress]) {
throw new Error(`No unlocker key or GCKMS key ID found for Oval address ${address}`);
}
return this.wallets[checkSummedAddress].connect(provider);
}
}
3 changes: 2 additions & 1 deletion src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface OvalConfig {
unlockerKey: string;
unlockerKey?: string;
gckmsKeyId?: string;
refundAddress: string;
refundPercent: number;
}
Expand Down
Loading

0 comments on commit d7ebab2

Please sign in to comment.