Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/outpost-client-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ The Ethereum client plugin is configured via program options as follows:
```
> NOTE: If you look closely, the reference to `eth-01` in the Ethereum client config, matches the signature provider configured for `Ethereum`. This mapping is what enables `1..n` clients in a single process

The signer reference is validated during startup. Each
`--outpost-ethereum-client` must reference the explicit, non-empty name of a
configured `--signature-provider`; anonymous signature-provider specs cannot be
referenced by an Ethereum client. When the optional Ethereum chain ID is
present, startup also calls `eth_chainId` on the configured RPC endpoint and
fails if the endpoint is unavailable, returns an invalid value, or reports a
different chain ID.

With the above configuration and the appropriate `app` & `plugin` config, you can access the `outpost-ethereum-client` configured with name/id == `eth-anvil-local` as follows

```cpp
Expand Down
164 changes: 164 additions & 0 deletions libraries/libfc-test/include/fc-test/one_shot_http_server.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#pragma once

#include <boost/asio.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>

#include <fc/io/json.hpp>

#include <cstdint>
#include <sstream>
#include <string>
#include <thread>
#include <utility>

namespace fc::test {

/**
* Minimal one-request loopback HTTP server for synchronous RPC client tests.
*
* The caller supplies the complete response body. The server accepts one
* request, returns HTTP 200 with that body, and closes the connection. Its
* destructor also unblocks an unused accept so tests that fail before making
* the expected request do not strand a worker thread.
*/
class one_shot_http_server {
public:
explicit one_shot_http_server(std::string response_body, std::string expected_rpc_method = {})
: _response_body(std::move(response_body))
, _expected_rpc_method(std::move(expected_rpc_method))
, _acceptor(_io, tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), 0))
, _port(_acceptor.local_endpoint().port())
, _worker([this] { serve(); }) {}

one_shot_http_server(const one_shot_http_server&) = delete;
one_shot_http_server& operator=(const one_shot_http_server&) = delete;

~one_shot_http_server() {
// Wake a worker blocked in synchronous accept without concurrently
// operating on the acceptor from two threads. The temporary socket is
// closed immediately, so a worker that accepts it exits its HTTP read.
boost::system::error_code error;
boost::asio::io_context io;
tcp::socket socket(io);
socket.connect(tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), _port), error);
socket.close(error);
if (_worker.joinable()) {
_worker.join();
}
_acceptor.close(error);
}

/** Return the loopback URL selected for this server. */
std::string url() const {
return "http://127.0.0.1:" + std::to_string(_port);
}

private:
using tcp = boost::asio::ip::tcp;

void serve() {
boost::system::error_code error;
tcp::socket socket(_io);
_acceptor.accept(socket, error);
if (error) {
return;
}

boost::beast::flat_buffer request_buffer;
boost::beast::http::request<boost::beast::http::string_body> request;
boost::beast::http::read(socket, request_buffer, request, error);
if (error) {
return;
}

if (!_expected_rpc_method.empty()) {
bool request_matches = false;
try {
const auto request_object = fc::json::from_string(request.body()).get_object();
request_matches =
request_object.contains("method") &&
request_object["method"].as_string() == _expected_rpc_method &&
request_object.contains("params") &&
request_object["params"].is_array() &&
request_object["params"].get_array().empty();
} catch (...) {
return;
}
if (!request_matches) {
return;
}
}

std::ostringstream response;
response << "HTTP/1.1 200 OK\r\n"
<< "Content-Type: application/json\r\n"
<< "Content-Length: " << _response_body.size() << "\r\n"
<< "Connection: close\r\n\r\n"
<< _response_body;
const auto response_text = response.str();
boost::asio::write(socket, boost::asio::buffer(response_text), error);
}

std::string _response_body;
std::string _expected_rpc_method;
boost::asio::io_context _io;
tcp::acceptor _acceptor;
uint16_t _port;
std::thread _worker;
};

/**
* Loopback server that deterministically accepts one connection and closes it.
*
* Unlike selecting and releasing an unused port, this fixture keeps ownership
* of the port until the client connects, so another process cannot race the
* test and unexpectedly provide a working endpoint.
*/
class connection_closing_http_server {
public:
connection_closing_http_server()
: _acceptor(_io, tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), 0))
, _port(_acceptor.local_endpoint().port())
, _worker([this] { close_connection(); }) {}

connection_closing_http_server(const connection_closing_http_server&) = delete;
connection_closing_http_server& operator=(const connection_closing_http_server&) = delete;

~connection_closing_http_server() {
boost::system::error_code error;
boost::asio::io_context io;
tcp::socket socket(io);
socket.connect(tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), _port), error);
socket.close(error);
if (_worker.joinable()) {
_worker.join();
}
_acceptor.close(error);
}

std::string url() const {
return "http://127.0.0.1:" + std::to_string(_port);
}

private:
using tcp = boost::asio::ip::tcp;

void close_connection() {
boost::system::error_code error;
tcp::socket socket(_io);
_acceptor.accept(socket, error);
if (error) {
return;
}
socket.set_option(boost::asio::socket_base::linger(true, 0), error);
socket.close(error);
}

boost::asio::io_context _io;
tcp::acceptor _acceptor;
uint16_t _port;
std::thread _worker;
};

} // namespace fc::test
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ struct ethereum_client_entry_t {
/// batch operator auto-select the client for an outpost row by matching the
/// row's `external_chain_id`, so multiple EVM outposts never share one
/// remote endpoint. `nullopt` when the spec omitted the chain id.
std::optional<uint64_t> chain_id;
std::optional<uint32_t> chain_id;
};

using ethereum_client_entry_ptr = std::shared_ptr<ethereum_client_entry_t>;
Expand Down Expand Up @@ -108,7 +108,7 @@ class outpost_ethereum_client_plugin : public appbase::plugin<outpost_ethereum_c
/// `external_chain_id`; an ambiguous (duplicate chain id) or missing match
/// yields nullptr so the caller can fail closed rather than relay an
/// outpost through the wrong endpoint.
ethereum_client_entry_ptr get_client_by_chain_id(uint64_t chain_id);
ethereum_client_entry_ptr get_client_by_chain_id(uint32_t chain_id);

const std::vector<std::pair<std::filesystem::path, std::vector<fc::network::ethereum::abi::contract>>>& get_abi_files();

Expand Down
Loading
Loading