diff --git a/src/external_services/bin/services_manager.rs b/src/external_services/bin/services_manager.rs index 35f65c5..983da6f 100644 --- a/src/external_services/bin/services_manager.rs +++ b/src/external_services/bin/services_manager.rs @@ -17,13 +17,17 @@ type HandlerFn = #[derive(Parser)] struct Cli { /// The rpc endpoint to connect to - #[arg(short, long, default_value_t = String::from("redis://127.0.0.1:6379/"))] + #[arg(long, default_value_t = String::from("redis://127.0.0.1:6379/"))] kv_redis_endpoint: String, - #[arg(short, long, default_value_t = String::from("redis://127.0.0.1:6379/"))] + #[arg(long, default_value_t = String::from("redis://127.0.0.1:6379/"))] pubsub_redis_endpoint: String, - #[arg(short, long, default_value_t = String::from("0.0.0.0"))] + + #[arg(long, default_values_t = vec![String::from("16813125:'http://127.0.0.1:8555'")])] + pub blockchain_rpc_endpoints: Vec, + + #[arg(long, default_value_t = String::from("0.0.0.0"))] host: String, - #[arg(short, long, default_value_t = String::from("5605"))] + #[arg(long, default_value_t = String::from("5605"))] port: String, } diff --git a/src/external_services/blockchain_rpc/RPC.sol b/src/external_services/blockchain_rpc/RPC.sol new file mode 100644 index 0000000..7e81664 --- /dev/null +++ b/src/external_services/blockchain_rpc/RPC.sol @@ -0,0 +1,37 @@ +pragma solidity ^0.8.19; + +import "../services_manager/ServicesManager.sol"; + +interface BlockchainRPC { + struct Config { + uint256 chainId; // The kettle operator provides the url, we can only specify for which chain! + } + + function raw_jsonrpc(string memory method, bytes[] memory params) external returns (bytes memory result); + function eth_call(address to, bytes memory input) external returns (bytes memory result); +} + +contract WithBlockchainRPC { + ExternalServiceImpl private impl; + constructor(uint256 chainId) { + bytes memory config = abi.encode(BlockchainRPC.Config(chainId)); + impl = new ExternalServiceImpl("blockchain_rpc", config); + } + + function rpc() internal view returns (BlockchainRPC) { + return BlockchainRPC(address(impl)); + } +} + +// Wraps an arbitrary interface to a contract on a different chain +contract EthCallWrapper is WithBlockchainRPC { + address wrappedContract; + + constructor(uint256 chainId, address _wrappedContract) WithBlockchainRPC(chainId) { + wrappedContract = _wrappedContract; + } + + fallback(bytes calldata cdata) external returns (bytes memory) { + return rpc().eth_call(wrappedContract, cdata); + } +} diff --git a/src/external_services/blockchain_rpc/SuaveRigil.sol b/src/external_services/blockchain_rpc/SuaveRigil.sol new file mode 100644 index 0000000..ec202a6 --- /dev/null +++ b/src/external_services/blockchain_rpc/SuaveRigil.sol @@ -0,0 +1,247 @@ +pragma solidity ^0.8.8; + +import "./RPC.sol"; + +// Drop-in replacement of Suave library to use with Andromeda. Simply link this library instead of the Rigil one. +library Suave { + error PeekerReverted(address, bytes); + + enum CryptoSignature { + SECP256, + BLS + } + + type DataId is bytes16; + + 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 DataRecord { + DataId id; + DataId salt; + uint64 decryptionCondition; + address[] allowedPeekers; + address[] allowedStores; + string version; + } + + struct HttpRequest { + string url; + string method; + string[] headers; + bytes body; + bool withFlashbotsSignature; + } + + struct SimulateTransactionResult { + uint64 egp; + SimulatedLog[] logs; + bool success; + string error; + } + + struct SimulatedLog { + bytes data; + address addr; + bytes32[] topics; + } + + struct Withdrawal { + uint64 index; + uint64 validator; + address Address; + uint64 amount; + } + + address public constant ANYALLOWED = 0xC8df3686b4Afb2BB53e60EAe97EF043FE03Fb829; + + address public constant IS_CONFIDENTIAL_ADDR = 0x0000000000000000000000000000000042010000; + + address public constant BUILD_ETH_BLOCK = 0x0000000000000000000000000000000042100001; + + address public constant CONFIDENTIAL_INPUTS = 0x0000000000000000000000000000000042010001; + + address public constant CONFIDENTIAL_RETRIEVE = 0x0000000000000000000000000000000042020001; + + address public constant CONFIDENTIAL_STORE = 0x0000000000000000000000000000000042020000; + + address public constant CONTEXT_GET = 0x0000000000000000000000000000000053300003; + + address public constant DO_HTTPREQUEST = 0x0000000000000000000000000000000043200002; + + address public constant ETHCALL = 0x0000000000000000000000000000000042100003; + + address public constant EXTRACT_HINT = 0x0000000000000000000000000000000042100037; + + address public constant FETCH_DATA_RECORDS = 0x0000000000000000000000000000000042030001; + + address public constant FILL_MEV_SHARE_BUNDLE = 0x0000000000000000000000000000000043200001; + + address public constant NEW_BUILDER = 0x0000000000000000000000000000000053200001; + + address public constant NEW_DATA_RECORD = 0x0000000000000000000000000000000042030000; + + address public constant PRIVATE_KEY_GEN = 0x0000000000000000000000000000000053200003; + + address public constant SIGN_ETH_TRANSACTION = 0x0000000000000000000000000000000040100001; + + address public constant SIGN_MESSAGE = 0x0000000000000000000000000000000040100003; + + address public constant SIMULATE_BUNDLE = 0x0000000000000000000000000000000042100000; + + address public constant SIMULATE_TRANSACTION = 0x0000000000000000000000000000000053200002; + + address public constant SUBMIT_BUNDLE_JSON_RPC = 0x0000000000000000000000000000000043000001; + + address public constant SUBMIT_ETH_BLOCK_TO_RELAY = 0x0000000000000000000000000000000042100002; + + // Returns whether execution is off- or on-chain + function isConfidential() internal returns (bool b) { + return abi.decode(_call(IS_CONFIDENTIAL_ADDR, ""), (bool)); + } + + function buildEthBlock(BuildBlockArgs memory blockArgs, DataId dataId, string memory namespace) + internal + returns (bytes memory, bytes memory) + { + return abi.decode(_call(BUILD_ETH_BLOCK, abi.encode(blockArgs, dataId, namespace)), (bytes, bytes)); + } + + function confidentialInputs() pure internal returns (bytes memory) { + // We are only servicig eth_calls, no confidential inputs are available in this context (on both sides) + revert PeekerReverted(CONFIDENTIAL_INPUTS, "not available in Andromeda context"); + } + + function confidentialRetrieve(DataId dataId, string memory key) internal returns (bytes memory) { + // Please use Andromeda's primitives instead + return abi.decode(_call(CONFIDENTIAL_RETRIEVE, abi.encode(dataId, key)), (bytes)); + } + + function confidentialStore(DataId dataId, string memory key, bytes memory value) internal { + // Please use Andromeda's primitives instead + _call(CONFIDENTIAL_STORE, abi.encode(dataId, key, value)); + } + + function contextGet(string memory key) internal returns (bytes memory) { + // Please use Andromeda's primitives instead + return abi.decode(_call(CONTEXT_GET, abi.encode(key)), (bytes)); + } + + function doHTTPRequest(HttpRequest memory request) internal returns (bytes memory) { + // TODO: should be moved to a native revm precompile + return abi.decode(_call(DO_HTTPREQUEST, abi.encode(request)), (bytes)); + } + + function ethcall(address contractAddr, bytes memory input1) internal returns (bytes memory) { + return abi.decode(_call(ETHCALL, abi.encode(contractAddr, input1)), (bytes)); + } + + function extractHint(bytes memory bundleData) internal returns (bytes memory) { + return abi.decode(_call(EXTRACT_HINT, abi.encode(bundleData)), (bytes)); + } + + function fetchDataRecords(uint64 cond, string memory namespace) internal returns (DataRecord[] memory) { + // Please use Andromeda's primitives instead + return abi.decode(_call(FETCH_DATA_RECORDS, abi.encode(cond, namespace)), (DataRecord[])); + } + + function fillMevShareBundle(DataId dataId) internal returns (bytes memory) { + return abi.decode(_call(FILL_MEV_SHARE_BUNDLE, abi.encode(dataId)), (bytes)); + } + + function newBuilder() internal returns (string memory) { + return abi.decode(_call(NEW_BUILDER, abi.encode()), (string)); + } + + function newDataRecord( + uint64 decryptionCondition, + address[] memory allowedPeekers, + address[] memory allowedStores, + string memory dataType + ) internal returns (DataRecord memory) { + return abi.decode(_call(NEW_DATA_RECORD, abi.encode(decryptionCondition, allowedPeekers, allowedStores, dataType)), (DataRecord)); + } + + function privateKeyGen(CryptoSignature crypto) internal returns (string memory) { + // Please use Andromeda's primitives instead + return abi.decode(_call(PRIVATE_KEY_GEN, abi.encode(crypto)), (string)); + } + + function signEthTransaction(bytes memory txn, string memory chainId, string memory signingKey) + internal + returns (bytes memory) + { + return abi.decode(_call(SIGN_ETH_TRANSACTION, abi.encode(txn, chainId, signingKey)), (bytes)); + } + + function signMessage(bytes memory digest, CryptoSignature crypto, string memory signingKey) + internal + returns (bytes memory) + { + return abi.decode(_call(SIGN_MESSAGE, abi.encode(digest, crypto, signingKey)), (bytes)); + } + + function simulateBundle(bytes memory bundleData) internal returns (uint64) { + return abi.decode(_call(SIMULATE_BUNDLE, abi.encode(bundleData)), (uint64)); + } + + function simulateTransaction(string memory sessionid, bytes memory txn) + internal + returns (SimulateTransactionResult memory) + { + return abi.decode(_call(SIMULATE_TRANSACTION, abi.encode(sessionid, txn)), (SimulateTransactionResult)); + } + + function submitBundleJsonRPC(string memory url, string memory method, bytes memory params) + internal + returns (bytes memory) + { + return abi.decode(_call(SUBMIT_BUNDLE_JSON_RPC, abi.encode(url, method, params)), (bytes)); + } + + function submitEthBlockToRelay(string memory relayUrl, bytes memory builderBid) internal returns (bytes memory) { + return abi.decode(_call(SUBMIT_ETH_BLOCK_TO_RELAY, abi.encode(relayUrl, builderBid)), (bytes)); + } + + // Glue code for the service handle + uint256 public constant RIGIL_CHAINID = 16813125; + + struct HandleStorage { + bytes32 _volatile_handle; + } + + function _handle() internal returns (bytes32) { + bytes32 pos = keccak256("handle.storage"); + HandleStorage storage hs; + assembly { hs.slot := pos } + + if (hs._volatile_handle == bytes32(0x0)) { + bytes memory config = abi.encode(BlockchainRPC.Config(RIGIL_CHAINID)); + (bool s_ok, bytes memory s_data) = SM_ADDR.staticcall(abi.encodeWithSelector(SM.getService.selector, "blockchain_rpc", config)); + require(s_ok, string(abi.encodePacked("getService for rigil rpc failed: ", string(s_data)))); + (bytes32 handle, bytes memory err) = abi.decode(s_data, (bytes32, bytes)); + require(err.length == 0, string(abi.encodePacked("could not initialize rigil rpc: ", string(err)))); + hs._volatile_handle = handle; + } + + return hs._volatile_handle; + } + + function _call(address precompile, bytes memory input) internal returns (bytes memory) { + bytes memory eth_call_data = abi.encodeWithSelector(BlockchainRPC.eth_call.selector, precompile, input); + (bool c_ok, bytes memory c_data) = SM_ADDR.staticcall(abi.encodeWithSelector(SM.callService.selector, _handle(), eth_call_data)); + require(c_ok, string(abi.encodePacked("rigil rpc call failed: ", string(c_data)))); + return c_data; + } +} diff --git a/src/external_services/blockchain_rpc/rpc.rs b/src/external_services/blockchain_rpc/rpc.rs new file mode 100644 index 0000000..23ee488 --- /dev/null +++ b/src/external_services/blockchain_rpc/rpc.rs @@ -0,0 +1,67 @@ +use ethers::abi::{encode, Contract, Detokenize, Token}; +use ethers::contract::{BaseContract, Lazy}; +use ethers::types::Bytes; + +use crate::external_services::common::CallContext; + +pub static RPC_ABI: Lazy = Lazy::new(|| { + let contract: Contract = + serde_json::from_str(include_str!("../../out/RPC.sol/BlockchainRPC.abi.json")).unwrap(); + BaseContract::from(contract) +}); + +pub fn rpc_contract() -> BaseContract { + RPC_ABI.clone() +} + +#[derive(Debug)] +pub enum RPCServiceError { + Error(String), + InstantiationError(String), + StreamError(String), + InvalidCall, + InvalidCalldata, + ConnectionFailure, +} + +pub struct RPCService { + pub abi: BaseContract, + + chain_endpoints: Vec<(U256, String)>, + + eth_call_fn_abi: ethers::abi::Function, +} + +impl RPCService { + pub fn new(chain_endpoints: Vec<(U256, String)>) -> Self { + let rpc_abi = rpc_contract.abi(); + + RedisService { + abi: rpc_contract(), + chain_endpoints, + eth_call_fn_abi: rpc_abi.function("eth_call").unwrap().clone(), + } + } + + pub fn eth_call( + &mut self, + context: CallContext, + inputs: &[u8], + ) -> Result { + let (method, params): (String, Vec) = Detokenize::from_tokens( + self.eth_call_fn_abi + .decode_input(inputs) + .map_err(|_e| RPCServiceError::InvalidCalldata)?, + ) + .map_err(|_e| RPCServiceError::InvalidCalldata)?; + + let res: Result, _> = self.client.get(&key); + match res { + Ok(value) => Ok(encode(&[Token::Bytes(value)])), + Err(e) => { + dbg!("redis: could not get {}: {}", &key, e); + Ok(encode(&[Token::Bytes(vec![])])) + } + } + } +} diff --git a/src/external_services/services_manager/services_manager.rs b/src/external_services/services_manager/services_manager.rs index ec9085f..53a3225 100644 --- a/src/external_services/services_manager/services_manager.rs +++ b/src/external_services/services_manager/services_manager.rs @@ -9,6 +9,7 @@ use ethers::types::{Bytes, H256}; use crate::external_services::common::CallContext; +use crate::external_services::blockchain_rpc::rpc::{RPCService, RPCServiceError}; use crate::external_services::builder::builder::{BuilderError, BuilderService}; use crate::external_services::redis::pubsub::{RedisPubsub, RedisPubsubError}; use crate::external_services::redis::redis::{RedisService, RedisServiceError}; @@ -35,6 +36,7 @@ pub struct ServicesManager { pub struct Config { pub kv_redis_endpoint: String, pub pubsub_redis_endpoint: String, + pub blockchain_rpc_endpoints: Vec<(U256, String)>, } impl ServicesManager { @@ -96,6 +98,9 @@ impl ServicesManager { ), #[cfg(not(feature = "redis_external_services"))] "redis" | "pubsub" => Err(ServiceError::ServiceUnavailable), + "blockchain_rpc" => Ok(Box::new(RPCService::new( + self.config.blockchain_rpc_endpoints.clone(), + )) as Box), "builder" => Ok(Box::new(BuilderService::new()) as Box), _ => Err(ServiceError::InvalidCall), }?; @@ -145,6 +150,7 @@ impl ServicesManager { pub enum ServiceError { RedisServiceError(RedisServiceError), RedisPubsubError(RedisPubsubError), + RPCServiceError(RPCServiceError), BuilderError(BuilderError), InstantiationError(String), @@ -246,6 +252,44 @@ impl Service for RedisPubsub { } } +impl Service for RPCService { + fn name(&self) -> String { + String::from("blockchain_rpc") + } + + fn function_name_from_selector(&self, selector: &[u8; 4]) -> Option { + match self.abi.methods.get(selector) { + Some((fn_name, _)) => Some(fn_name.clone()), + None => None, + } + } + + fn instantiate(&self, config: Bytes) -> Result<(), ServiceError> { + if config.len() == 0 { + return Err(ServiceError::InstantiationError(String::from( + "missing config", + ))); + } + + println!("instantiated blockchain_rpc with {:?}", self.abi.methods); + + Ok(()) + } + + fn call( + &mut self, + fn_name: &str, + context: CallContext, + inputs: &[u8], + ) -> Result { + match fn_name { + "eth_call" => self.eth_call(context, inputs), + _ => Err(RPCServiceError::InvalidCall), + } + .map_err(|e| ServiceError::RPCServiceError(e)) + } +} + impl Service for BuilderService { fn name(&self) -> String { String::from("builder") diff --git a/src/lib.rs b/src/lib.rs index 5c91bba..104ee5c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,9 @@ pub mod external_services { pub mod pubsub; pub mod redis; } + pub mod blockchain_rpc { + pub mod rpc; + }; pub mod services_manager { pub mod services_manager; } diff --git a/src/out/RPC.sol/BlockchainRPC.abi.json b/src/out/RPC.sol/BlockchainRPC.abi.json new file mode 100644 index 0000000..5daabc0 --- /dev/null +++ b/src/out/RPC.sol/BlockchainRPC.abi.json @@ -0,0 +1,50 @@ +[ + { + "type": "function", + "name": "eth_call", + "inputs": [ + { + "name": "to", + "type": "address", + "internalType": "address" + }, + { + "name": "input", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "result", + "type": "bytes", + "internalType": "bytes" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "raw_jsonrpc", + "inputs": [ + { + "name": "method", + "type": "string", + "internalType": "string" + }, + { + "name": "params", + "type": "bytes[]", + "internalType": "bytes[]" + } + ], + "outputs": [ + { + "name": "result", + "type": "bytes", + "internalType": "bytes" + } + ], + "stateMutability": "nonpayable" + } +] \ No newline at end of file