Skip to content

Commit

Permalink
Add builder api (#47)
Browse files Browse the repository at this point in the history
* Add builder api

* Add more changes

* Add bid

* Parse root bid

* Fix tests for builder api client
  • Loading branch information
ferranbt authored May 2, 2024
1 parent 5121ca7 commit f7c2f02
Show file tree
Hide file tree
Showing 15 changed files with 578 additions and 3 deletions.
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

SUAVEX_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
SUAVEX_RPC_URL=http://localhost:8545

deploy-suavex-contract:
forge create \
--rpc-url $(SUAVEX_RPC_URL) \
--private-key $(SUAVEX_PRIVATE_KEY) \
test/protocols/Builder/Session.t.sol:Example --json | jq -r ".deployedTo"
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[profile.default]
runs = 10_000
solc_version = "0.8.23"
fs_permissions = [{ access = "read", path = "./test" }]
ast = true
[profile.suave]
whitelist = ["*"]
43 changes: 43 additions & 0 deletions src/Transactions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity ^0.8.13;
import "./utils/RLPWriter.sol";
import "./suavelib/Suave.sol";
import "Solidity-RLP/RLPReader.sol";
import "solady/src/utils/LibString.sol";

/// @notice Transactions is a library with utilities to encode, decode and sign Ethereum transactions.
library Transactions {
Expand Down Expand Up @@ -433,4 +434,46 @@ library Transactions {
v := byte(0, mload(add(signature, 0x60)))
}
}

function encodeJSON(EIP155 memory txn) internal pure returns (bytes memory) {
// encode transaction in json
bytes memory txnEncoded;

// dynamic fields
if (txn.data.length != 0) {
txnEncoded = abi.encodePacked(txnEncoded, '"input":"', LibString.toHexString(txn.data), '",');
} else {
txnEncoded = abi.encodePacked(txnEncoded, '"input":"0x",');
}
if (txn.to != address(0)) {
txnEncoded = abi.encodePacked(txnEncoded, '"to":"', LibString.toHexString(txn.to), '",');
} else {
txnEncoded = abi.encodePacked(txnEncoded, '"to":null,');
}

// fixed fields
txnEncoded = abi.encodePacked(
txnEncoded,
'"gas":"',
LibString.toMinimalHexString(txn.gas),
'","gasPrice":"',
LibString.toMinimalHexString(txn.gasPrice),
'","nonce":"',
LibString.toMinimalHexString(txn.nonce),
'","value":"',
LibString.toMinimalHexString(txn.value),
'","chainId":"',
LibString.toMinimalHexString(txn.chainId),
'","r":"',
LibString.toHexString(abi.encodePacked(txn.r)),
'","s":"',
LibString.toHexString(abi.encodePacked(txn.s)),
'",'
);

txnEncoded = abi.encodePacked(txnEncoded, '"v":"', LibString.toMinimalHexString(txn.v), '"');
txnEncoded = abi.encodePacked("{", txnEncoded, "}");

return txnEncoded;
}
}
74 changes: 74 additions & 0 deletions src/protocols/Builder/Session.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../../suavelib/Suave.sol";
import "../../Transactions.sol";
import "solady/src/utils/LibString.sol";
import {Types} from "./Types.sol";
import "solady/src/utils/JSONParserLib.sol";
import "../../utils/HexStrings.sol";

contract Session is Test {
using JSONParserLib for *;

string url;
string session;

constructor(string memory _url) {
url = _url;
}

function start(Types.BuildBlockArgs memory args) public {
bytes memory encoded = Types.encodeBuildBlockArgs(args);
JSONParserLib.Item memory output = callImpl("newSession", encoded);
session = output.value();
}

function addTransaction(Transactions.EIP155 memory txn) public returns (Types.SimulateTransactionResult memory) {
bytes memory encoded = Transactions.encodeJSON(txn);

bytes memory input = abi.encodePacked(session, ",", encoded);
JSONParserLib.Item memory output = callImpl("addTransaction", input);

Types.SimulateTransactionResult memory result = Types.decodeSimulateTransactionResult(output.value());
return result;
}

function buildBlock() public {
bytes memory input = abi.encodePacked(session);
callImpl("buildBlock", input);
}

function bid(string memory blsPubKey) public returns (JSONParserLib.Item memory) {
bytes memory input = abi.encodePacked(session, ',"0x', blsPubKey, '"');
JSONParserLib.Item memory output = callImpl("bid", input);

console.log("-- bid output --");
console.log(output.value());

// retrieve the root to sign
bytes memory root = HexStrings.fromHexString(HexStrings.stripQuotesAndPrefix(output.at('"root"').value()));
}

function callImpl(string memory method, bytes memory args) internal returns (JSONParserLib.Item memory) {
Suave.HttpRequest memory request;
request.method = "POST";
request.url = url;
request.headers = new string[](1);
request.headers[0] = "Content-Type: application/json";

bytes memory body =
abi.encodePacked('{"jsonrpc":"2.0","method":"suavex_', method, '","params":[', args, '],"id":1}');
request.body = body;

console.log(string(body));

bytes memory output = Suave.doHTTPRequest(request);

console.log(string(output));

JSONParserLib.Item memory item = string(output).parse();
return item.at('"result"');
}
}
238 changes: 238 additions & 0 deletions src/protocols/Builder/Types.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.13;

import "../../suavelib/Suave.sol";
import "../../Transactions.sol";
import "solady/src/utils/LibString.sol";
import "solady/src/utils/JSONParserLib.sol";

library Types {
using JSONParserLib for *;

struct SimulateTransactionResult {
uint64 egp;
SimulatedLog[] logs;
bool success;
string error;
}

struct SimulatedLog {
bytes data;
address addr;
bytes32[] topics;
}

struct BuildBlockArgs {
uint64 slot;
bytes proposerPubkey;
bytes32 parent;
uint64 timestamp;
address feeRecipient;
uint64 gasLimit;
bytes32 random;
Withdrawal[] withdrawals;
bytes extra;
bytes32 beaconRoot;
bool fillPending;
}

struct Withdrawal {
uint64 index;
uint64 validator;
address Address;
uint64 amount;
}

// encodeBuildBlockArgs encodes BuildBlockArgs to json
function encodeBuildBlockArgs(BuildBlockArgs memory args) internal returns (bytes memory) {
bytes memory body = abi.encodePacked(
'{"slot":"',
LibString.toMinimalHexString(args.slot),
'","proposerPubkey":"',
LibString.toHexString(args.proposerPubkey),
'"',
',"parent":"',
LibString.toHexString(abi.encodePacked(args.parent)),
'"',
',"timestamp":"',
LibString.toHexString(args.timestamp)
);

body = abi.encodePacked(
body,
'","feeRecipient":"',
LibString.toHexStringChecksummed(args.feeRecipient),
'"',
',"gasLimit":"',
LibString.toHexString(args.gasLimit),
'","random":"',
LibString.toHexString(abi.encodePacked(args.random)),
'"'
);

body = abi.encodePacked(
body,
',"withdrawals":',
encodeWithdrawals(args.withdrawals),
',"extra":"',
LibString.toHexString(args.extra),
'"',
',"beaconRoot":"',
LibString.toHexString(abi.encodePacked(args.beaconRoot)),
'"',
',"fillPending":',
args.fillPending ? "true" : "false",
"}"
);
return body;
}

// encodeWithdrawals encodes Withdrawal array to json
function encodeWithdrawals(Withdrawal[] memory withdrawals) internal returns (bytes memory) {
bytes memory result = abi.encodePacked("[");
for (uint64 i = 0; i < withdrawals.length; i++) {
result = abi.encodePacked(result, i > 0 ? "," : "", encodeWithdrawal(withdrawals[i]));
}
return abi.encodePacked(result, "]");
}

// encodeWithdrawal encodes Withdrawal to json
function encodeWithdrawal(Withdrawal memory withdrawal) internal returns (bytes memory) {
return abi.encodePacked(
'{"index":',
LibString.toHexString(withdrawal.index),
',"validator":',
LibString.toHexString(withdrawal.validator),
',"Address":"',
LibString.toHexStringChecksummed(withdrawal.Address),
'"',
',"amount":',
LibString.toHexString(withdrawal.amount),
"}"
);
}

function decodeSimulateTransactionResult(string memory input)
internal
returns (SimulateTransactionResult memory result)
{
JSONParserLib.Item memory item = input.parse();
return decodeSimulateTransactionResult(item);
}

function decodeSimulatedLog(JSONParserLib.Item memory item) internal returns (SimulatedLog memory log) {
log.data = fromHexString(_stripQuotesAndPrefix(item.at('"data"').value()));
log.addr = bytesToAddress(fromHexString(_stripQuotesAndPrefix(item.at('"addr"').value())));

JSONParserLib.Item[] memory topics = item.at('"topics"').children();
log.topics = new bytes32[](topics.length);
for (uint64 i = 0; i < topics.length; i++) {
log.topics[i] = bytesToBytes32(fromHexString(_stripQuotesAndPrefix(topics[i].value())));
}
}

function decodeSimulateTransactionResult(JSONParserLib.Item memory item)
internal
returns (SimulateTransactionResult memory result)
{
if (compareStrings(item.at('"success"').value(), "true")) {
result.success = true;
} else {
result.success = false;
result.error = trimQuotes(item.at('"error"').value());
}

// decode logs
JSONParserLib.Item[] memory logs = item.at('"logs"').children();
result.logs = new SimulatedLog[](logs.length);
for (uint64 i = 0; i < logs.length; i++) {
result.logs[i] = decodeSimulatedLog(logs[i]);
}
}

function compareStrings(string memory a, string memory b) internal pure returns (bool) {
// Check if the lengths of the strings are the same
if (bytes(a).length != bytes(b).length) {
return false;
} else {
// Compare each character of the strings
for (uint256 i = 0; i < bytes(a).length; i++) {
if (bytes(a)[i] != bytes(b)[i]) {
return false;
}
}
return true;
}
}

function trimQuotes(string memory input) private pure returns (string memory) {
bytes memory inputBytes = bytes(input);
require(
inputBytes.length >= 2 && inputBytes[0] == '"' && inputBytes[inputBytes.length - 1] == '"', "Invalid input"
);

bytes memory result = new bytes(inputBytes.length - 2);

for (uint256 i = 1; i < inputBytes.length - 1; i++) {
result[i - 1] = inputBytes[i];
}

return string(result);
}

function _fromHexChar(uint8 c) internal pure returns (uint8) {
if (bytes1(c) >= bytes1("0") && bytes1(c) <= bytes1("9")) {
return c - uint8(bytes1("0"));
}
if (bytes1(c) >= bytes1("a") && bytes1(c) <= bytes1("f")) {
return 10 + c - uint8(bytes1("a"));
}
if (bytes1(c) >= bytes1("A") && bytes1(c) <= bytes1("F")) {
return 10 + c - uint8(bytes1("A"));
}
revert("fail");
}

function _stripQuotesAndPrefix(string memory s) internal pure returns (string memory) {
bytes memory strBytes = bytes(s);
bytes memory result = new bytes(strBytes.length - 4);
for (uint256 i = 3; i < strBytes.length - 1; i++) {
result[i - 3] = strBytes[i];
}
return string(result);
}

// Convert an hexadecimal string to raw bytes
function fromHexString(string memory s) internal pure returns (bytes memory) {
bytes memory ss = bytes(s);
require(ss.length % 2 == 0); // length must be even
bytes memory r = new bytes(ss.length / 2);
for (uint256 i = 0; i < ss.length / 2; ++i) {
r[i] = bytes1(_fromHexChar(uint8(ss[2 * i])) * 16 + _fromHexChar(uint8(ss[2 * i + 1])));
}
return r;
}

function bytesToAddress(bytes memory data) public pure returns (address) {
// Ensure data length is at least 20 bytes (address length)
require(data.length >= 20, "Invalid data length");

address addr;
// Convert bytes to address
assembly {
addr := mload(add(data, 20))
}
return addr;
}

function bytesToBytes32(bytes memory data) public pure returns (bytes32) {
require(data.length >= 32, "Data length must be at least 32 bytes");

bytes32 result;
assembly {
// Copy 32 bytes from data to result
result := mload(add(data, 32))
}
return result;
}
}
Loading

0 comments on commit f7c2f02

Please sign in to comment.