diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b739c4f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@main + - uses: cachix/cachix-action@v15 + with: + name: logos-co + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + # libverifproxy is a ~25 minute source build of the whole nimbus/Nim + # toolchain, so the Logos cache is doing real work here rather than + # shaving seconds. + - name: Build module + run: nix build -L + + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@main + - uses: cachix/cachix-action@v15 + with: + name: logos-co + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + # The check derivation RUNS the suite as part of building it, so a red + # test is a failed build. Named explicitly rather than via `nix flake + # check`, which would also evaluate the x86_64-windows pseudo-system. + - name: Unit tests + run: | + nix build -L ".#checks.$(nix eval --impure --raw --expr builtins.currentSystem).unit-tests" + + codegen: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # The 60 typed eth_*/op_* wrappers are generated from the library's own + # dispatch table but COMMITTED as literal text, because the module's code + # generator parses verified_proxy_impl.h as text and cannot see through a + # macro. This proves nobody hand-edited the generated blocks, and that a + # bumped nimbus input has not left them behind. + - name: RPC wrappers match the generator + run: python3 tools/gen_rpc_methods.py --check diff --git a/CMakeLists.txt b/CMakeLists.txt index 8efabee..09ec275 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,7 @@ logos_module( SOURCES src/verified_proxy_impl.h src/verified_proxy_impl.cpp + src/verified_proxy_rpc.cpp src/proxy_config.h src/proxy_config.cpp src/proxy_runtime.h diff --git a/README.md b/README.md index 9563d98..43b174b 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,8 @@ impossible without it. (Infura notably does not.) | `supportedNetworks()` | The accepted networks with their chain ids and a default endpoint pair. Build a UI selector from this, not a hardcoded list. | | `fetchFinalizedRoot(beaconUrl)` | Convenience: asks a beacon node for its current finalized root. **Not** a trust anchor — see below. | | `rpc(method, params)` | Any method the proxy supports. `params` is a JSON-RPC array. | -| `ethBlockNumber()`, `ethGetBalance(...)`, `ethCall(...)`, … | Typed wrappers over the same path. | +| `ethBlockNumber()`, `ethGetBalance(...)`, `ethCall(...)`, … | 30 typed `eth*` wrappers over the same path. | +| `opBlockNumber()`, `opGetBalance(...)`, … | The 30 `op*` mirrors. Need an OP-Stack network and `opExecutionApiUrls`. | Events: `proxyStarted`, `proxyStopped`, `proxyStateChanged`. diff --git a/src/verified_proxy_impl.cpp b/src/verified_proxy_impl.cpp index ae858e1..a104166 100644 --- a/src/verified_proxy_impl.cpp +++ b/src/verified_proxy_impl.cpp @@ -284,40 +284,3 @@ StdLogosResult VerifiedProxyImpl::fetchFinalizedRoot(const std::string& beaconUr StdLogosResult VerifiedProxyImpl::rpc(const std::string& method, const LogosList& params) { return m_rt->call(method, params.is_null() ? json::array() : params); } - -StdLogosResult VerifiedProxyImpl::ethBlockNumber() { - return m_rt->call("eth_blockNumber", json::array()); -} - -StdLogosResult VerifiedProxyImpl::ethChainId() { - return m_rt->call("eth_chainId", json::array()); -} - -StdLogosResult VerifiedProxyImpl::ethGetBalance(const std::string& address, - const std::string& blockTag) { - return m_rt->call("eth_getBalance", json::array({ address, blockTag })); -} - -StdLogosResult VerifiedProxyImpl::ethGetCode(const std::string& address, - const std::string& blockTag) { - return m_rt->call("eth_getCode", json::array({ address, blockTag })); -} - -StdLogosResult VerifiedProxyImpl::ethGetBlockByNumber(const std::string& blockTag, - bool fullTransactions) { - return m_rt->call("eth_getBlockByNumber", json::array({ blockTag, fullTransactions })); -} - -StdLogosResult VerifiedProxyImpl::ethCall(const LogosMap& txArgs, - const std::string& blockTag, - bool optimisticStateFetch) { - // The third positional parameter is upstream's own extension; a standard - // JSON-RPC eth_call has only two. - return m_rt->call("eth_call", json::array({ txArgs, blockTag, optimisticStateFetch })); -} - -StdLogosResult VerifiedProxyImpl::ethGetTransactionByBlockNumberAndIndex( - const std::string& blockTag, uint64_t index) { - return m_rt->call("eth_getTransactionByBlockNumberAndIndex", - json::array({ blockTag, index })); -} diff --git a/src/verified_proxy_impl.h b/src/verified_proxy_impl.h index 3f3d365..d2e1aab 100644 --- a/src/verified_proxy_impl.h +++ b/src/verified_proxy_impl.h @@ -202,35 +202,297 @@ public: /// /// Returns the decoded result value on success. StdLogosResult rpc(const std::string& method, const LogosList& params); - - /// Current verified head block number. - /// - /// Returns a JSON **number**, not a hex quantity string — upstream's - /// encoding is not uniform (`ethChainId` and `ethGasPrice` do return hex - /// strings). Measured against sepolia, not inferred from the JSON-RPC spec. - StdLogosResult ethBlockNumber(); - - /// The chain id the proxy is configured for, as a hex quantity string. + // BEGIN GENERATED RPC WRAPPERS -- edit tools/gen_rpc_methods.py, not this + /// `eth_chainId`, verified. StdLogosResult ethChainId(); - /// Verified account balance in wei, as a hex quantity string. - /// `blockTag` is "latest", "pending", "earliest", or a hex block number. + /// `eth_blockNumber`, verified. + StdLogosResult ethBlockNumber(); + + /// `eth_getBalance`, verified. StdLogosResult ethGetBalance(const std::string& address, const std::string& blockTag); - /// Verified contract code at `address`, as a hex byte string. + /// `eth_getStorageAt`, verified. + StdLogosResult ethGetStorageAt(const std::string& address, const std::string& slot, const std::string& blockTag); + + /// `eth_getTransactionCount`, verified. + StdLogosResult ethGetTransactionCount(const std::string& address, const std::string& blockTag); + + /// `eth_getCode`, verified. StdLogosResult ethGetCode(const std::string& address, const std::string& blockTag); - /// Verified block. `fullTransactions` selects full objects over hashes. + /// `eth_getBlockByHash`, verified. + StdLogosResult ethGetBlockByHash(const std::string& blockHash, bool fullTransactions); + + /// `eth_getBlockByNumber`, verified. StdLogosResult ethGetBlockByNumber(const std::string& blockTag, bool fullTransactions); - /// Verified `eth_call`. `txArgs` is a transaction object ({to, data, ...}). - /// `optimisticStateFetch` trades a stricter state check for latency. - StdLogosResult ethCall(const LogosMap& txArgs, const std::string& blockTag, - bool optimisticStateFetch); + /// `eth_getUncleCountByBlockNumber`, verified. + StdLogosResult ethGetUncleCountByBlockNumber(const std::string& blockTag); + + /// `eth_getUncleCountByBlockHash`, verified. + StdLogosResult ethGetUncleCountByBlockHash(const std::string& blockHash); + + /// `eth_getBlockTransactionCountByNumber`, verified. + StdLogosResult ethGetBlockTransactionCountByNumber(const std::string& blockTag); + + /// `eth_getBlockTransactionCountByHash`, verified. + StdLogosResult ethGetBlockTransactionCountByHash(const std::string& blockHash); + + /// `eth_getTransactionByBlockNumberAndIndex`, verified. + StdLogosResult ethGetTransactionByBlockNumberAndIndex(const std::string& blockTag, uint64_t index); + + /// `eth_getTransactionByBlockHashAndIndex`, verified. + StdLogosResult ethGetTransactionByBlockHashAndIndex(const std::string& blockHash, uint64_t index); + + /// `eth_call`, verified. + /// + /// `optimisticStateFetch` is upstream's own extension to the standard + /// JSON-RPC signature, not a parameter callers will know from elsewhere. + StdLogosResult ethCall(const LogosMap& txArgs, const std::string& blockTag, bool optimisticStateFetch); + + /// `eth_createAccessList`, verified. + /// + /// `optimisticStateFetch` is upstream's own extension to the standard + /// JSON-RPC signature, not a parameter callers will know from elsewhere. + StdLogosResult ethCreateAccessList(const LogosMap& txArgs, const std::string& blockTag, bool optimisticStateFetch); + + /// `eth_estimateGas`, verified. + /// + /// `optimisticStateFetch` is upstream's own extension to the standard + /// JSON-RPC signature, not a parameter callers will know from elsewhere. + StdLogosResult ethEstimateGas(const LogosMap& txArgs, const std::string& blockTag, bool optimisticStateFetch); + + /// `eth_getTransactionByHash`, verified. + StdLogosResult ethGetTransactionByHash(const std::string& txHash); + + /// `eth_getBlockReceipts`, verified. + StdLogosResult ethGetBlockReceipts(const std::string& blockTag); + + /// `eth_getTransactionReceipt`, verified. + StdLogosResult ethGetTransactionReceipt(const std::string& txHash); + + /// `eth_getLogs`, verified. + StdLogosResult ethGetLogs(const LogosMap& filterOptions); + + /// `eth_newFilter`, verified. + StdLogosResult ethNewFilter(const LogosMap& filterOptions); + + /// `eth_uninstallFilter`, verified. + StdLogosResult ethUninstallFilter(const std::string& filterId); + + /// `eth_getFilterLogs`, verified. + StdLogosResult ethGetFilterLogs(const std::string& filterId); + + /// `eth_getFilterChanges`, verified. + StdLogosResult ethGetFilterChanges(const std::string& filterId); + + /// `eth_blobBaseFee`, verified. + StdLogosResult ethBlobBaseFee(); + + /// `eth_gasPrice`, verified. + StdLogosResult ethGasPrice(); + + /// `eth_maxPriorityFeePerGas`, verified. + StdLogosResult ethMaxPriorityFeePerGas(); + + /// `eth_feeHistory`, verified. + StdLogosResult ethFeeHistory(uint64_t blockCount, const std::string& newestBlock, const LogosList& rewardPercentiles); + + /// `eth_sendRawTransaction`, verified. + StdLogosResult ethSendRawTransaction(const std::string& txHexBytes); + + /// `op_chainId`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opChainId(); + + /// `op_blockNumber`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opBlockNumber(); + + /// `op_getBalance`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opGetBalance(const std::string& address, const std::string& blockTag); + + /// `op_getStorageAt`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opGetStorageAt(const std::string& address, const std::string& slot, const std::string& blockTag); + + /// `op_getTransactionCount`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opGetTransactionCount(const std::string& address, const std::string& blockTag); + + /// `op_getCode`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opGetCode(const std::string& address, const std::string& blockTag); + + /// `op_getBlockByHash`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opGetBlockByHash(const std::string& blockHash, bool fullTransactions); + + /// `op_getBlockByNumber`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opGetBlockByNumber(const std::string& blockTag, bool fullTransactions); + + /// `op_getUncleCountByBlockNumber`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opGetUncleCountByBlockNumber(const std::string& blockTag); + + /// `op_getUncleCountByBlockHash`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opGetUncleCountByBlockHash(const std::string& blockHash); + + /// `op_getBlockTransactionCountByNumber`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opGetBlockTransactionCountByNumber(const std::string& blockTag); + + /// `op_getBlockTransactionCountByHash`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opGetBlockTransactionCountByHash(const std::string& blockHash); + + /// `op_getTransactionByBlockNumberAndIndex`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opGetTransactionByBlockNumberAndIndex(const std::string& blockTag, uint64_t index); + + /// `op_getTransactionByBlockHashAndIndex`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opGetTransactionByBlockHashAndIndex(const std::string& blockHash, uint64_t index); + + /// `op_call`, verified. + /// + /// `optimisticStateFetch` is upstream's own extension to the standard + /// JSON-RPC signature, not a parameter callers will know from elsewhere. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opCall(const LogosMap& txArgs, const std::string& blockTag, bool optimisticStateFetch); + + /// `op_createAccessList`, verified. + /// + /// `optimisticStateFetch` is upstream's own extension to the standard + /// JSON-RPC signature, not a parameter callers will know from elsewhere. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opCreateAccessList(const LogosMap& txArgs, const std::string& blockTag, bool optimisticStateFetch); + + /// `op_estimateGas`, verified. + /// + /// `optimisticStateFetch` is upstream's own extension to the standard + /// JSON-RPC signature, not a parameter callers will know from elsewhere. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opEstimateGas(const LogosMap& txArgs, const std::string& blockTag, bool optimisticStateFetch); + + /// `op_getTransactionByHash`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opGetTransactionByHash(const std::string& txHash); + + /// `op_getBlockReceipts`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opGetBlockReceipts(const std::string& blockTag); + + /// `op_getTransactionReceipt`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opGetTransactionReceipt(const std::string& txHash); + + /// `op_getLogs`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opGetLogs(const LogosMap& filterOptions); + + /// `op_newFilter`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opNewFilter(const LogosMap& filterOptions); + + /// `op_uninstallFilter`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opUninstallFilter(const std::string& filterId); + + /// `op_getFilterLogs`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opGetFilterLogs(const std::string& filterId); + + /// `op_getFilterChanges`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opGetFilterChanges(const std::string& filterId); + + /// `op_blobBaseFee`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opBlobBaseFee(); + + /// `op_gasPrice`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opGasPrice(); + + /// `op_maxPriorityFeePerGas`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opMaxPriorityFeePerGas(); + + /// `op_feeHistory`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opFeeHistory(uint64_t blockCount, const std::string& newestBlock, const LogosList& rewardPercentiles); + + /// `op_sendRawTransaction`, verified. + /// + /// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the + /// library answers with a clear error rather than a wrong value. + StdLogosResult opSendRawTransaction(const std::string& txHexBytes); + + // END GENERATED RPC WRAPPERS - /// Verified transaction by index within a block. - StdLogosResult ethGetTransactionByBlockNumberAndIndex(const std::string& blockTag, - uint64_t index); logos_events: /// Emitted when start() finishes. {"success":bool,"chainId":number,"error":string} diff --git a/src/verified_proxy_rpc.cpp b/src/verified_proxy_rpc.cpp new file mode 100644 index 0000000..fadc58d --- /dev/null +++ b/src/verified_proxy_rpc.cpp @@ -0,0 +1,260 @@ +// The typed eth_*/op_* surface. +// +// Every one of these is three lines over the SAME dispatch path: the library's +// proxyCall is a string `case` over the very procs its typed C entry points +// call, so there is one FFI path here rather than sixty-one. They exist for +// discoverability — `lm methods`, the LIDL contract, and a caller's type +// checker — not because each needs its own binding. +// +// Generated. See tools/gen_rpc_methods.py; the table comes from the library's +// own dispatch table, so a bumped nimbus input cannot silently leave this +// behind. + +#include "verified_proxy_impl.h" + +#include + +using json = nlohmann::json; + +// BEGIN GENERATED RPC WRAPPERS -- edit tools/gen_rpc_methods.py, not this +StdLogosResult VerifiedProxyImpl::ethChainId() { + return rpc("eth_chainId", json::array({})); +} + +StdLogosResult VerifiedProxyImpl::ethBlockNumber() { + return rpc("eth_blockNumber", json::array({})); +} + +StdLogosResult VerifiedProxyImpl::ethGetBalance(const std::string& address, const std::string& blockTag) { + return rpc("eth_getBalance", json::array({address, blockTag})); +} + +StdLogosResult VerifiedProxyImpl::ethGetStorageAt(const std::string& address, const std::string& slot, const std::string& blockTag) { + return rpc("eth_getStorageAt", json::array({address, slot, blockTag})); +} + +StdLogosResult VerifiedProxyImpl::ethGetTransactionCount(const std::string& address, const std::string& blockTag) { + return rpc("eth_getTransactionCount", json::array({address, blockTag})); +} + +StdLogosResult VerifiedProxyImpl::ethGetCode(const std::string& address, const std::string& blockTag) { + return rpc("eth_getCode", json::array({address, blockTag})); +} + +StdLogosResult VerifiedProxyImpl::ethGetBlockByHash(const std::string& blockHash, bool fullTransactions) { + return rpc("eth_getBlockByHash", json::array({blockHash, fullTransactions})); +} + +StdLogosResult VerifiedProxyImpl::ethGetBlockByNumber(const std::string& blockTag, bool fullTransactions) { + return rpc("eth_getBlockByNumber", json::array({blockTag, fullTransactions})); +} + +StdLogosResult VerifiedProxyImpl::ethGetUncleCountByBlockNumber(const std::string& blockTag) { + return rpc("eth_getUncleCountByBlockNumber", json::array({blockTag})); +} + +StdLogosResult VerifiedProxyImpl::ethGetUncleCountByBlockHash(const std::string& blockHash) { + return rpc("eth_getUncleCountByBlockHash", json::array({blockHash})); +} + +StdLogosResult VerifiedProxyImpl::ethGetBlockTransactionCountByNumber(const std::string& blockTag) { + return rpc("eth_getBlockTransactionCountByNumber", json::array({blockTag})); +} + +StdLogosResult VerifiedProxyImpl::ethGetBlockTransactionCountByHash(const std::string& blockHash) { + return rpc("eth_getBlockTransactionCountByHash", json::array({blockHash})); +} + +StdLogosResult VerifiedProxyImpl::ethGetTransactionByBlockNumberAndIndex(const std::string& blockTag, uint64_t index) { + return rpc("eth_getTransactionByBlockNumberAndIndex", json::array({blockTag, index})); +} + +StdLogosResult VerifiedProxyImpl::ethGetTransactionByBlockHashAndIndex(const std::string& blockHash, uint64_t index) { + return rpc("eth_getTransactionByBlockHashAndIndex", json::array({blockHash, index})); +} + +StdLogosResult VerifiedProxyImpl::ethCall(const LogosMap& txArgs, const std::string& blockTag, bool optimisticStateFetch) { + return rpc("eth_call", json::array({txArgs, blockTag, optimisticStateFetch})); +} + +StdLogosResult VerifiedProxyImpl::ethCreateAccessList(const LogosMap& txArgs, const std::string& blockTag, bool optimisticStateFetch) { + return rpc("eth_createAccessList", json::array({txArgs, blockTag, optimisticStateFetch})); +} + +StdLogosResult VerifiedProxyImpl::ethEstimateGas(const LogosMap& txArgs, const std::string& blockTag, bool optimisticStateFetch) { + return rpc("eth_estimateGas", json::array({txArgs, blockTag, optimisticStateFetch})); +} + +StdLogosResult VerifiedProxyImpl::ethGetTransactionByHash(const std::string& txHash) { + return rpc("eth_getTransactionByHash", json::array({txHash})); +} + +StdLogosResult VerifiedProxyImpl::ethGetBlockReceipts(const std::string& blockTag) { + return rpc("eth_getBlockReceipts", json::array({blockTag})); +} + +StdLogosResult VerifiedProxyImpl::ethGetTransactionReceipt(const std::string& txHash) { + return rpc("eth_getTransactionReceipt", json::array({txHash})); +} + +StdLogosResult VerifiedProxyImpl::ethGetLogs(const LogosMap& filterOptions) { + return rpc("eth_getLogs", json::array({filterOptions})); +} + +StdLogosResult VerifiedProxyImpl::ethNewFilter(const LogosMap& filterOptions) { + return rpc("eth_newFilter", json::array({filterOptions})); +} + +StdLogosResult VerifiedProxyImpl::ethUninstallFilter(const std::string& filterId) { + return rpc("eth_uninstallFilter", json::array({filterId})); +} + +StdLogosResult VerifiedProxyImpl::ethGetFilterLogs(const std::string& filterId) { + return rpc("eth_getFilterLogs", json::array({filterId})); +} + +StdLogosResult VerifiedProxyImpl::ethGetFilterChanges(const std::string& filterId) { + return rpc("eth_getFilterChanges", json::array({filterId})); +} + +StdLogosResult VerifiedProxyImpl::ethBlobBaseFee() { + return rpc("eth_blobBaseFee", json::array({})); +} + +StdLogosResult VerifiedProxyImpl::ethGasPrice() { + return rpc("eth_gasPrice", json::array({})); +} + +StdLogosResult VerifiedProxyImpl::ethMaxPriorityFeePerGas() { + return rpc("eth_maxPriorityFeePerGas", json::array({})); +} + +StdLogosResult VerifiedProxyImpl::ethFeeHistory(uint64_t blockCount, const std::string& newestBlock, const LogosList& rewardPercentiles) { + return rpc("eth_feeHistory", json::array({blockCount, newestBlock, rewardPercentiles})); +} + +StdLogosResult VerifiedProxyImpl::ethSendRawTransaction(const std::string& txHexBytes) { + return rpc("eth_sendRawTransaction", json::array({txHexBytes})); +} + +StdLogosResult VerifiedProxyImpl::opChainId() { + return rpc("op_chainId", json::array({})); +} + +StdLogosResult VerifiedProxyImpl::opBlockNumber() { + return rpc("op_blockNumber", json::array({})); +} + +StdLogosResult VerifiedProxyImpl::opGetBalance(const std::string& address, const std::string& blockTag) { + return rpc("op_getBalance", json::array({address, blockTag})); +} + +StdLogosResult VerifiedProxyImpl::opGetStorageAt(const std::string& address, const std::string& slot, const std::string& blockTag) { + return rpc("op_getStorageAt", json::array({address, slot, blockTag})); +} + +StdLogosResult VerifiedProxyImpl::opGetTransactionCount(const std::string& address, const std::string& blockTag) { + return rpc("op_getTransactionCount", json::array({address, blockTag})); +} + +StdLogosResult VerifiedProxyImpl::opGetCode(const std::string& address, const std::string& blockTag) { + return rpc("op_getCode", json::array({address, blockTag})); +} + +StdLogosResult VerifiedProxyImpl::opGetBlockByHash(const std::string& blockHash, bool fullTransactions) { + return rpc("op_getBlockByHash", json::array({blockHash, fullTransactions})); +} + +StdLogosResult VerifiedProxyImpl::opGetBlockByNumber(const std::string& blockTag, bool fullTransactions) { + return rpc("op_getBlockByNumber", json::array({blockTag, fullTransactions})); +} + +StdLogosResult VerifiedProxyImpl::opGetUncleCountByBlockNumber(const std::string& blockTag) { + return rpc("op_getUncleCountByBlockNumber", json::array({blockTag})); +} + +StdLogosResult VerifiedProxyImpl::opGetUncleCountByBlockHash(const std::string& blockHash) { + return rpc("op_getUncleCountByBlockHash", json::array({blockHash})); +} + +StdLogosResult VerifiedProxyImpl::opGetBlockTransactionCountByNumber(const std::string& blockTag) { + return rpc("op_getBlockTransactionCountByNumber", json::array({blockTag})); +} + +StdLogosResult VerifiedProxyImpl::opGetBlockTransactionCountByHash(const std::string& blockHash) { + return rpc("op_getBlockTransactionCountByHash", json::array({blockHash})); +} + +StdLogosResult VerifiedProxyImpl::opGetTransactionByBlockNumberAndIndex(const std::string& blockTag, uint64_t index) { + return rpc("op_getTransactionByBlockNumberAndIndex", json::array({blockTag, index})); +} + +StdLogosResult VerifiedProxyImpl::opGetTransactionByBlockHashAndIndex(const std::string& blockHash, uint64_t index) { + return rpc("op_getTransactionByBlockHashAndIndex", json::array({blockHash, index})); +} + +StdLogosResult VerifiedProxyImpl::opCall(const LogosMap& txArgs, const std::string& blockTag, bool optimisticStateFetch) { + return rpc("op_call", json::array({txArgs, blockTag, optimisticStateFetch})); +} + +StdLogosResult VerifiedProxyImpl::opCreateAccessList(const LogosMap& txArgs, const std::string& blockTag, bool optimisticStateFetch) { + return rpc("op_createAccessList", json::array({txArgs, blockTag, optimisticStateFetch})); +} + +StdLogosResult VerifiedProxyImpl::opEstimateGas(const LogosMap& txArgs, const std::string& blockTag, bool optimisticStateFetch) { + return rpc("op_estimateGas", json::array({txArgs, blockTag, optimisticStateFetch})); +} + +StdLogosResult VerifiedProxyImpl::opGetTransactionByHash(const std::string& txHash) { + return rpc("op_getTransactionByHash", json::array({txHash})); +} + +StdLogosResult VerifiedProxyImpl::opGetBlockReceipts(const std::string& blockTag) { + return rpc("op_getBlockReceipts", json::array({blockTag})); +} + +StdLogosResult VerifiedProxyImpl::opGetTransactionReceipt(const std::string& txHash) { + return rpc("op_getTransactionReceipt", json::array({txHash})); +} + +StdLogosResult VerifiedProxyImpl::opGetLogs(const LogosMap& filterOptions) { + return rpc("op_getLogs", json::array({filterOptions})); +} + +StdLogosResult VerifiedProxyImpl::opNewFilter(const LogosMap& filterOptions) { + return rpc("op_newFilter", json::array({filterOptions})); +} + +StdLogosResult VerifiedProxyImpl::opUninstallFilter(const std::string& filterId) { + return rpc("op_uninstallFilter", json::array({filterId})); +} + +StdLogosResult VerifiedProxyImpl::opGetFilterLogs(const std::string& filterId) { + return rpc("op_getFilterLogs", json::array({filterId})); +} + +StdLogosResult VerifiedProxyImpl::opGetFilterChanges(const std::string& filterId) { + return rpc("op_getFilterChanges", json::array({filterId})); +} + +StdLogosResult VerifiedProxyImpl::opBlobBaseFee() { + return rpc("op_blobBaseFee", json::array({})); +} + +StdLogosResult VerifiedProxyImpl::opGasPrice() { + return rpc("op_gasPrice", json::array({})); +} + +StdLogosResult VerifiedProxyImpl::opMaxPriorityFeePerGas() { + return rpc("op_maxPriorityFeePerGas", json::array({})); +} + +StdLogosResult VerifiedProxyImpl::opFeeHistory(uint64_t blockCount, const std::string& newestBlock, const LogosList& rewardPercentiles) { + return rpc("op_feeHistory", json::array({blockCount, newestBlock, rewardPercentiles})); +} + +StdLogosResult VerifiedProxyImpl::opSendRawTransaction(const std::string& txHexBytes) { + return rpc("op_sendRawTransaction", json::array({txHexBytes})); +} + +// END GENERATED RPC WRAPPERS diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a478106..f700c7b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -22,6 +22,7 @@ logos_test( ../src/proxy_config.cpp ../src/proxy_runtime.cpp ../src/verified_proxy_impl.cpp + ../src/verified_proxy_rpc.cpp ../src/rpc_http_server.cpp ../src/beacon_client.cpp TEST_SOURCES diff --git a/tools/gen_rpc_methods.py b/tools/gen_rpc_methods.py new file mode 100644 index 0000000..4f36f67 --- /dev/null +++ b/tools/gen_rpc_methods.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Generate the typed eth_*/op_* wrappers from the library's own dispatch table. + +The wrappers are three lines each over one shared `rpc()` path, so hand-writing +sixty-one of them would be sixty-one chances to transpose an argument. They are +generated instead — but COMMITTED as literal text, because the module's code +generator parses verified_proxy_impl.h as TEXT to build the LIDL contract and +would not see anything hidden behind a macro. + +Run with --check in CI to prove the committed blocks still match. + +The table below was extracted mechanically from nimbus-eth1's +nimbus_verified_proxy/library/c_frontend.nim (the `case $name` inside proxyCall) +at the revision this module pins. Re-extract with --from-source +after bumping that input. +""" +import argparse +import json +import re +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +SRC = HERE.parent / "src" + +BEGIN = "// BEGIN GENERATED RPC WRAPPERS -- edit tools/gen_rpc_methods.py, not this" +END = "// END GENERATED RPC WRAPPERS" + +# Parameter names, per RPC method, in order. Only the eth_ spellings are listed; +# the op_ mirror reuses them. Names reach the LIDL contract and `lm methods`, so +# they are the API's documentation as much as its signature. +PARAM_NAMES = { + "getBalance": ["address", "blockTag"], + "getCode": ["address", "blockTag"], + "getTransactionCount": ["address", "blockTag"], + "getStorageAt": ["address", "slot", "blockTag"], + "getBlockByNumber": ["blockTag", "fullTransactions"], + "getBlockByHash": ["blockHash", "fullTransactions"], + "getUncleCountByBlockNumber": ["blockTag"], + "getUncleCountByBlockHash": ["blockHash"], + "getBlockTransactionCountByNumber": ["blockTag"], + "getBlockTransactionCountByHash": ["blockHash"], + "getTransactionByBlockNumberAndIndex": ["blockTag", "index"], + "getTransactionByBlockHashAndIndex": ["blockHash", "index"], + "getTransactionByHash": ["txHash"], + "getTransactionReceipt": ["txHash"], + "getBlockReceipts": ["blockTag"], + "call": ["txArgs", "blockTag", "optimisticStateFetch"], + "estimateGas": ["txArgs", "blockTag", "optimisticStateFetch"], + "createAccessList": ["txArgs", "blockTag", "optimisticStateFetch"], + "getLogs": ["filterOptions"], + "newFilter": ["filterOptions"], + "uninstallFilter": ["filterId"], + "getFilterLogs": ["filterId"], + "getFilterChanges": ["filterId"], + "feeHistory": ["blockCount", "newestBlock", "rewardPercentiles"], + "sendRawTransaction": ["txHexBytes"], +} + +# The one JSON parameter that is an ARRAY rather than an object. Everything else +# reaching the library as a raw JSON node is a transaction or filter object. +JSON_LISTS = {("feeHistory", 2)} + +CPP_TYPE = { + "str": "const std::string& ", + "bool": "bool ", + "u64": "uint64_t ", +} + +# Methods this module drives itself; a typed wrapper would invite callers to +# fight the runtime for control of them. +SKIP = {"eth_syncing"} + + +def extract(path): + body = Path(path).read_text() + body = body[body.index("proc proxyCall("):] + out = [] + for block in re.split(r'\n of "', body)[1:]: + name = block.split('"', 1)[0] + seg = block.split('\n of "')[0] + kinds = [] + for m in re.finditer( + r"parsedParams\[(\d+)\]\.(getStr|getBool|getBiggestInt)\(\)" + r"|\(\$parsedParams\[(\d+)\]\)", seg): + if m.group(2): + kinds.append((int(m.group(1)), + {"getStr": "str", "getBool": "bool", + "getBiggestInt": "u64"}[m.group(2)])) + else: + kinds.append((int(m.group(3)), "json")) + out.append({"rpc": name, "params": [k for _, k in sorted(set(kinds))]}) + return out + + +def cpp_name(rpc): + prefix, rest = rpc.split("_", 1) + return prefix + rest[0].upper() + rest[1:] + + +def stem(rpc): + return rpc.split("_", 1)[1] + + +def signature(rpc, params): + base = stem(rpc) + names = PARAM_NAMES.get(base, [f"arg{i}" for i in range(len(params))]) + args = [] + for i, kind in enumerate(params): + if kind == "json": + t = "const LogosList& " if (base, i) in JSON_LISTS else "const LogosMap& " + else: + t = CPP_TYPE[kind] + args.append(t + names[i]) + return f"StdLogosResult {cpp_name(rpc)}({', '.join(args)})" + + +def doc(rpc, params): + base = stem(rpc) + lines = [f"/// `{rpc}`, verified."] + if base in ("call", "estimateGas", "createAccessList"): + lines.append("///") + lines.append("/// `optimisticStateFetch` is upstream's own extension to the standard") + lines.append("/// JSON-RPC signature, not a parameter callers will know from elsewhere.") + if rpc.startswith("op_"): + lines.append("///") + lines.append("/// Requires an OP-Stack network and `opExecutionApiUrls`; otherwise the") + lines.append("/// library answers with a clear error rather than a wrong value.") + return lines + + +def emit_header(table): + out = [BEGIN] + for e in table: + out += [" " + l for l in doc(e["rpc"], e["params"])] + out.append(f" {signature(e['rpc'], e['params'])};") + out.append("") + out.append(" " + END) + return "\n".join(out) + + +def emit_impl(table): + out = [BEGIN] + for e in table: + base, params = stem(e["rpc"]), e["params"] + names = PARAM_NAMES.get(base, [f"arg{i}" for i in range(len(params))]) + sig = signature(e["rpc"], params).replace( + "StdLogosResult ", "StdLogosResult VerifiedProxyImpl::", 1) + args = ", ".join(names) + out.append(f"{sig} {{") + out.append(f' return rpc("{e["rpc"]}", json::array({{{args}}}));') + out.append("}") + out.append("") + out.append(END) + return "\n".join(out) + + +def splice(path, block): + text = Path(path).read_text() + i, j = text.index(BEGIN), text.index(END) + len(END) + return text[:i] + block.strip() + text[j:] + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--from-source", help="path to c_frontend.nim to re-extract the table") + ap.add_argument("--check", action="store_true", help="fail if the committed files differ") + a = ap.parse_args() + + tbl_path = HERE / "rpc_methods.json" + if a.from_source: + table = extract(a.from_source) + tbl_path.write_text(json.dumps(table, indent=1) + "\n") + table = json.loads(tbl_path.read_text()) + table = [e for e in table if e["rpc"] not in SKIP] + + targets = { + SRC / "verified_proxy_impl.h": emit_header(table), + SRC / "verified_proxy_rpc.cpp": emit_impl(table), + } + bad = False + for path, block in targets.items(): + new = splice(path, block) + if a.check: + if new != path.read_text(): + print(f"DRIFT: {path.name} does not match the generator", file=sys.stderr) + bad = True + else: + path.write_text(new) + if a.check and bad: + print("run: python3 tools/gen_rpc_methods.py", file=sys.stderr) + return 1 + print(f"{len(table)} wrappers {'checked' if a.check else 'generated'}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/rpc_methods.json b/tools/rpc_methods.json new file mode 100644 index 0000000..e0cb284 --- /dev/null +++ b/tools/rpc_methods.json @@ -0,0 +1,380 @@ +[ + { + "rpc": "eth_chainId", + "params": [] + }, + { + "rpc": "eth_blockNumber", + "params": [] + }, + { + "rpc": "eth_syncing", + "params": [] + }, + { + "rpc": "eth_getBalance", + "params": [ + "str", + "str" + ] + }, + { + "rpc": "eth_getStorageAt", + "params": [ + "str", + "str", + "str" + ] + }, + { + "rpc": "eth_getTransactionCount", + "params": [ + "str", + "str" + ] + }, + { + "rpc": "eth_getCode", + "params": [ + "str", + "str" + ] + }, + { + "rpc": "eth_getBlockByHash", + "params": [ + "str", + "bool" + ] + }, + { + "rpc": "eth_getBlockByNumber", + "params": [ + "str", + "bool" + ] + }, + { + "rpc": "eth_getUncleCountByBlockNumber", + "params": [ + "str" + ] + }, + { + "rpc": "eth_getUncleCountByBlockHash", + "params": [ + "str" + ] + }, + { + "rpc": "eth_getBlockTransactionCountByNumber", + "params": [ + "str" + ] + }, + { + "rpc": "eth_getBlockTransactionCountByHash", + "params": [ + "str" + ] + }, + { + "rpc": "eth_getTransactionByBlockNumberAndIndex", + "params": [ + "str", + "u64" + ] + }, + { + "rpc": "eth_getTransactionByBlockHashAndIndex", + "params": [ + "str", + "u64" + ] + }, + { + "rpc": "eth_call", + "params": [ + "json", + "str", + "bool" + ] + }, + { + "rpc": "eth_createAccessList", + "params": [ + "json", + "str", + "bool" + ] + }, + { + "rpc": "eth_estimateGas", + "params": [ + "json", + "str", + "bool" + ] + }, + { + "rpc": "eth_getTransactionByHash", + "params": [ + "str" + ] + }, + { + "rpc": "eth_getBlockReceipts", + "params": [ + "str" + ] + }, + { + "rpc": "eth_getTransactionReceipt", + "params": [ + "str" + ] + }, + { + "rpc": "eth_getLogs", + "params": [ + "json" + ] + }, + { + "rpc": "eth_newFilter", + "params": [ + "json" + ] + }, + { + "rpc": "eth_uninstallFilter", + "params": [ + "str" + ] + }, + { + "rpc": "eth_getFilterLogs", + "params": [ + "str" + ] + }, + { + "rpc": "eth_getFilterChanges", + "params": [ + "str" + ] + }, + { + "rpc": "eth_blobBaseFee", + "params": [] + }, + { + "rpc": "eth_gasPrice", + "params": [] + }, + { + "rpc": "eth_maxPriorityFeePerGas", + "params": [] + }, + { + "rpc": "eth_feeHistory", + "params": [ + "u64", + "str", + "json" + ] + }, + { + "rpc": "eth_sendRawTransaction", + "params": [ + "str" + ] + }, + { + "rpc": "op_chainId", + "params": [] + }, + { + "rpc": "op_blockNumber", + "params": [] + }, + { + "rpc": "op_getBalance", + "params": [ + "str", + "str" + ] + }, + { + "rpc": "op_getStorageAt", + "params": [ + "str", + "str", + "str" + ] + }, + { + "rpc": "op_getTransactionCount", + "params": [ + "str", + "str" + ] + }, + { + "rpc": "op_getCode", + "params": [ + "str", + "str" + ] + }, + { + "rpc": "op_getBlockByHash", + "params": [ + "str", + "bool" + ] + }, + { + "rpc": "op_getBlockByNumber", + "params": [ + "str", + "bool" + ] + }, + { + "rpc": "op_getUncleCountByBlockNumber", + "params": [ + "str" + ] + }, + { + "rpc": "op_getUncleCountByBlockHash", + "params": [ + "str" + ] + }, + { + "rpc": "op_getBlockTransactionCountByNumber", + "params": [ + "str" + ] + }, + { + "rpc": "op_getBlockTransactionCountByHash", + "params": [ + "str" + ] + }, + { + "rpc": "op_getTransactionByBlockNumberAndIndex", + "params": [ + "str", + "u64" + ] + }, + { + "rpc": "op_getTransactionByBlockHashAndIndex", + "params": [ + "str", + "u64" + ] + }, + { + "rpc": "op_call", + "params": [ + "json", + "str", + "bool" + ] + }, + { + "rpc": "op_createAccessList", + "params": [ + "json", + "str", + "bool" + ] + }, + { + "rpc": "op_estimateGas", + "params": [ + "json", + "str", + "bool" + ] + }, + { + "rpc": "op_getTransactionByHash", + "params": [ + "str" + ] + }, + { + "rpc": "op_getBlockReceipts", + "params": [ + "str" + ] + }, + { + "rpc": "op_getTransactionReceipt", + "params": [ + "str" + ] + }, + { + "rpc": "op_getLogs", + "params": [ + "json" + ] + }, + { + "rpc": "op_newFilter", + "params": [ + "json" + ] + }, + { + "rpc": "op_uninstallFilter", + "params": [ + "str" + ] + }, + { + "rpc": "op_getFilterLogs", + "params": [ + "str" + ] + }, + { + "rpc": "op_getFilterChanges", + "params": [ + "str" + ] + }, + { + "rpc": "op_blobBaseFee", + "params": [] + }, + { + "rpc": "op_gasPrice", + "params": [] + }, + { + "rpc": "op_maxPriorityFeePerGas", + "params": [] + }, + { + "rpc": "op_feeHistory", + "params": [ + "u64", + "str", + "json" + ] + }, + { + "rpc": "op_sendRawTransaction", + "params": [ + "str" + ] + } +] \ No newline at end of file