diff --git a/.gitignore b/.gitignore index 4605856c..8041f277 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,25 @@ .gitconfig + res/ target/ deps/ data/ + .idea/ .vscode/ + rocksdb sequencer/service/data/ storage.json + result + wallet-ffi/wallet_ffi.h bedrock_signing_key integration_tests/configs/debug/ venv/ + keycard_wallet/python/__pycache__/ keycard_wallet/python/keycard-py/ + +.DS_Store diff --git a/Cargo.lock b/Cargo.lock index ee5c97f2..9af3ff27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3823,16 +3823,14 @@ dependencies = [ name = "indexer_ffi" version = "0.1.0" dependencies = [ - "anyhow", "cbindgen", - "indexer_service", + "env_logger", + "futures", + "indexer_core", "indexer_service_protocol", - "indexer_service_rpc", - "jsonrpsee", "lee", "log", "tokio", - "url", ] [[package]] diff --git a/integration_tests/tests/indexer_ffi_block_batching.rs b/integration_tests/tests/indexer_ffi_block_batching.rs index eeef276d..52f68027 100644 --- a/integration_tests/tests/indexer_ffi_block_batching.rs +++ b/integration_tests/tests/indexer_ffi_block_batching.rs @@ -5,7 +5,7 @@ )] use anyhow::Result; -use indexer_ffi::{Runtime, api::types::FfiOption}; +use indexer_ffi::api::types::FfiOption; use integration_tests::L2_TO_L1_TIMEOUT; use log::info; @@ -14,21 +14,21 @@ mod indexer_ffi_helpers; #[test] fn indexer_ffi_block_batching() -> Result<()> { - let (ctx, indexer_ffi, _indexer_dir) = indexer_ffi_helpers::setup()?; + // `_ctx` keeps the bedrock/sequencer harness (and its runtime) alive for the + // duration of the test; the indexer was started on that runtime. + let (_ctx, indexer_ffi, _indexer_dir) = indexer_ffi_helpers::setup()?; // WAIT info!("Waiting for indexer to parse blocks"); std::thread::sleep(L2_TO_L1_TIMEOUT); - // Safety: ctx runtime is valid for the lifetime of the returned Runtime - let runtime = unsafe { Runtime::from_borrowed(ctx.runtime()) }; - let last_block_indexer_ffi_res = unsafe { - indexer_ffi_helpers::query_last_block(&raw const runtime, &raw const indexer_ffi) - }; + let last_block_indexer_ffi_res = + unsafe { indexer_ffi_helpers::query_last_block(&raw const indexer_ffi) }; assert!(last_block_indexer_ffi_res.error.is_ok()); + assert!(last_block_indexer_ffi_res.is_some); - let last_block_indexer = unsafe { *last_block_indexer_ffi_res.value }; + let last_block_indexer = last_block_indexer_ffi_res.block_id; info!("Last block on indexer FFI now is {last_block_indexer}"); @@ -37,14 +37,8 @@ fn indexer_ffi_block_batching() -> Result<()> { let before_ffi = FfiOption::::from_none(); let limit = 100; - let block_batch_ffi_res = unsafe { - indexer_ffi_helpers::query_block_vec( - &raw const runtime, - &raw const indexer_ffi, - before_ffi, - limit, - ) - }; + let block_batch_ffi_res = + unsafe { indexer_ffi_helpers::query_block_vec(&raw const indexer_ffi, before_ffi, limit) }; assert!(block_batch_ffi_res.error.is_ok()); diff --git a/integration_tests/tests/indexer_ffi_helpers/mod.rs b/integration_tests/tests/indexer_ffi_helpers/mod.rs index c3c3caff..35bf7ef3 100644 --- a/integration_tests/tests/indexer_ffi_helpers/mod.rs +++ b/integration_tests/tests/indexer_ffi_helpers/mod.rs @@ -13,6 +13,7 @@ use indexer_ffi::{ api::{ PointerResult, lifecycle::InitializedIndexerServiceFFIResult, + query::LastBlockIdResult, types::{FfiAccountId, FfiOption, FfiVec, account::FfiAccount, block::FfiBlock}, }, }; @@ -20,20 +21,15 @@ use integration_tests::{BlockingTestContext, TestContext}; use tempfile::TempDir; unsafe extern "C" { - pub unsafe fn query_last_block( - runtime: *const Runtime, - indexer: *const IndexerServiceFFI, - ) -> PointerResult; + pub unsafe fn query_last_block(indexer: *const IndexerServiceFFI) -> LastBlockIdResult; pub unsafe fn query_block_vec( - runtime: *const Runtime, indexer: *const IndexerServiceFFI, before: FfiOption, limit: u64, ) -> PointerResult, OperationStatus>; pub unsafe fn query_account( - runtime: *const Runtime, indexer: *const IndexerServiceFFI, account_id: FfiAccountId, ) -> PointerResult; @@ -41,7 +37,6 @@ unsafe extern "C" { pub unsafe fn start_indexer( runtime: *const Runtime, config_path: *const c_char, - port: u16, ) -> InitializedIndexerServiceFFIResult; } @@ -69,7 +64,7 @@ pub fn setup_indexer_ffi( let res = // SAFETY: lib function ensures validity of value. - unsafe { start_indexer(std::ptr::from_ref(runtime), CString::new(config_path.to_str().unwrap())?.as_ptr(), 0) }; + unsafe { start_indexer(std::ptr::from_ref(runtime), CString::new(config_path.to_str().unwrap())?.as_ptr()) }; if res.error.is_error() { anyhow::bail!("Indexer FFI error {:?}", res.error); diff --git a/integration_tests/tests/indexer_test_run_ffi.rs b/integration_tests/tests/indexer_test_run_ffi.rs index 2c7c2103..0a5f4133 100644 --- a/integration_tests/tests/indexer_test_run_ffi.rs +++ b/integration_tests/tests/indexer_test_run_ffi.rs @@ -5,7 +5,6 @@ )] use anyhow::Result; -use indexer_ffi::Runtime; use integration_tests::L2_TO_L1_TIMEOUT; use log::info; @@ -14,20 +13,20 @@ mod indexer_ffi_helpers; #[test] fn indexer_test_run_ffi() -> Result<()> { - let (ctx, indexer_ffi, _indexer_dir) = indexer_ffi_helpers::setup()?; + // `_ctx` keeps the bedrock/sequencer harness (and its runtime) alive for the + // duration of the test; the indexer was started on that runtime. + let (_ctx, indexer_ffi, _indexer_dir) = indexer_ffi_helpers::setup()?; // RUN OBSERVATION std::thread::sleep(L2_TO_L1_TIMEOUT); - // Safety: ctx runtime is valid for the lifetime of the returned Runtime - let runtime = unsafe { Runtime::from_borrowed(ctx.runtime()) }; - let last_block_indexer_ffi_res = unsafe { - indexer_ffi_helpers::query_last_block(&raw const runtime, &raw const indexer_ffi) - }; + let last_block_indexer_ffi_res = + unsafe { indexer_ffi_helpers::query_last_block(&raw const indexer_ffi) }; assert!(last_block_indexer_ffi_res.error.is_ok()); + assert!(last_block_indexer_ffi_res.is_some); - let last_block_indexer_ffi = unsafe { *last_block_indexer_ffi_res.value }; + let last_block_indexer_ffi = last_block_indexer_ffi_res.block_id; info!("Last block on indexer FFI now is {last_block_indexer_ffi}"); diff --git a/lez/indexer/ffi/Cargo.toml b/lez/indexer/ffi/Cargo.toml index 66f6a518..50748d56 100644 --- a/lez/indexer/ffi/Cargo.toml +++ b/lez/indexer/ffi/Cargo.toml @@ -6,15 +6,15 @@ version = "0.1.0" [dependencies] lee.workspace = true -indexer_service.workspace = true -indexer_service_rpc = { workspace = true, features = ["client"] } -indexer_service_protocol.workspace = true +indexer_core.workspace = true +# `convert` brings in the LEE <-> protocol `From`/`TryFrom` impls the query +# marshalling relies on (previously pulled in transitively via `indexer_service`). +indexer_service_protocol = { workspace = true, features = ["convert"] } -url.workspace = true +env_logger.workspace = true log = { workspace = true } tokio = { features = ["rt-multi-thread"], workspace = true } -jsonrpsee.workspace = true -anyhow.workspace = true +futures.workspace = true [build-dependencies] cbindgen = "0.29" diff --git a/lez/indexer/ffi/build.rs b/lez/indexer/ffi/build.rs index 92c95407..349ff38d 100644 --- a/lez/indexer/ffi/build.rs +++ b/lez/indexer/ffi/build.rs @@ -6,6 +6,7 @@ fn main() { cbindgen::Builder::new() .with_crate(crate_dir) .with_language(cbindgen::Language::C) + .with_pragma_once(true) .generate() .expect("Unable to generate bindings") .write_to_file("indexer_ffi.h"); diff --git a/lez/indexer/ffi/cbindgen.toml b/lez/indexer/ffi/cbindgen.toml deleted file mode 100644 index 79f622b7..00000000 --- a/lez/indexer/ffi/cbindgen.toml +++ /dev/null @@ -1,2 +0,0 @@ -language = "C" # For increased compatibility -no_includes = true diff --git a/lez/indexer/ffi/indexer_ffi.h b/lez/indexer/ffi/indexer_ffi.h index 84aaeae7..06953e39 100644 --- a/lez/indexer/ffi/indexer_ffi.h +++ b/lez/indexer/ffi/indexer_ffi.h @@ -1,3 +1,5 @@ +#pragma once + #include #include #include @@ -22,11 +24,19 @@ typedef enum FfiBedrockStatus { Finalized, } FfiBedrockStatus; -typedef struct Option_u64 Option_u64; - +/** + * FFI-owned indexer. + * + * Has three fields behind `c_void` (so that cbindgen never needs to see their Rust layout): + * - An [`IndexerCore`] used to answer queries + * - The background task [`JoinHandle`] that drives ingestion (consuming the block stream so the + * store stays populated) + * - A [`Handle`] to the runtime they live on. + */ typedef struct IndexerServiceFFI { - void *indexer_handle; - void *indexer_client; + void *core; + void *ingest_handle; + void *runtime_handle; } IndexerServiceFFI; /** @@ -83,15 +93,18 @@ typedef struct PointerResult_Runtime__OperationStatus { } PointerResult_Runtime__OperationStatus; /** - * Simple wrapper around a pointer to a value or an error. + * Result of [`query_last_block`], returned **inline** (no heap allocation, so + * there is no corresponding `free_*` to call). * - * Pointer is not guaranteed. You should check the error field before - * dereferencing the pointer. + * `block_id` is only meaningful when `error` is `Ok` *and* `is_some` is + * `true`. An `Ok` result with `is_some == false` means the indexer has no + * finalized block yet (an empty chain) — which is distinct from an error. */ -typedef struct PointerResult_Option_u64_____OperationStatus { - struct Option_u64 *value; +typedef struct LastBlockIdResult { + uint64_t block_id; + bool is_some; enum OperationStatus error; -} PointerResult_Option_u64_____OperationStatus; +} LastBlockIdResult; typedef uint64_t FfiBlockId; @@ -411,7 +424,6 @@ typedef struct PointerResult_FfiVec_FfiTransaction_____OperationStatus { * # Arguments * * - `config_path`: A pointer to a string representing the path to the configuration file. - * - `port`: Number representing a port, on which indexers RPC will start. * * # Returns * @@ -424,8 +436,7 @@ typedef struct PointerResult_FfiVec_FfiTransaction_____OperationStatus { * - `config_path` is a valid pointer to a null-terminated C string. */ InitializedIndexerServiceFFIResult start_indexer(const struct Runtime *runtime, - const char *config_path, - uint16_t port); + const char *config_path); /** * Creates a new [`tokio::runtime::Runtime`]. @@ -452,6 +463,20 @@ struct PointerResult_Runtime__OperationStatus new_runtime(void); */ enum OperationStatus stop_indexer(struct IndexerServiceFFI *indexer); +/** + * Initializes the FFI's logger. + * + * Wires up `env_logger`, so the library's `log::*` output is controlled by the + * `RUST_LOG` environment variable (e.g. `RUST_LOG=info`). Without this, the + * FFI's log calls go nowhere — and since failures are otherwise reported only + * as numeric [`OperationStatus`](crate::errors::OperationStatus) codes, there + * is no other way to see *why* a call failed. + * + * Safe to call multiple times and from any consumer: if a global logger is + * already set, the call is a no-op. + */ +void init_logger(void); + /** * # Safety * It's up to the caller to pass a proper pointer, if somehow from c/c++ side @@ -469,16 +494,15 @@ void free_cstring(char *block); * * # Returns * - * A `PointerResult, OperationStatus>` indicating success or failure. + * A [`LastBlockIdResult`] indicating success or failure. The block id is + * returned inline; nothing needs to be freed. * * # Safety * * The caller must ensure that: - * - `runtime` is a valid pointer to a [`Runtime`] instance. * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. */ -struct PointerResult_Option_u64_____OperationStatus query_last_block(const struct Runtime *runtime, - const struct IndexerServiceFFI *indexer); +struct LastBlockIdResult query_last_block(const struct IndexerServiceFFI *indexer); /** * Query the block by id from indexer. @@ -495,15 +519,13 @@ struct PointerResult_Option_u64_____OperationStatus query_last_block(const struc * # Safety * * The caller must ensure that: - * - `runtime` is a valid pointer to a [`Runtime`] instance. * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. */ -struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct Runtime *runtime, - const struct IndexerServiceFFI *indexer, +struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct IndexerServiceFFI *indexer, FfiBlockId block_id); /** - * Query the block by id from indexer. + * Query the block by hash from indexer. * * # Arguments * @@ -517,11 +539,9 @@ struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct Runti * # Safety * * The caller must ensure that: - * - `runtime` is a valid pointer to a [`Runtime`] instance. * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. */ -struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const struct Runtime *runtime, - const struct IndexerServiceFFI *indexer, +struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const struct IndexerServiceFFI *indexer, FfiHashType hash); /** @@ -539,15 +559,13 @@ struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const stru * # Safety * * The caller must ensure that: - * - `runtime` is a valid pointer to a [`Runtime`] instance. * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. */ -struct PointerResult_FfiAccount__OperationStatus query_account(const struct Runtime *runtime, - const struct IndexerServiceFFI *indexer, +struct PointerResult_FfiAccount__OperationStatus query_account(const struct IndexerServiceFFI *indexer, FfiAccountId account_id); /** - * Query the trasnaction by hash from indexer. + * Query the transaction by hash from indexer. * * # Arguments * @@ -562,10 +580,8 @@ struct PointerResult_FfiAccount__OperationStatus query_account(const struct Runt * * The caller must ensure that: * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. - * - `runtime` is a valid pointer to a [`Runtime`] instance. */ -struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transaction(const struct Runtime *runtime, - const struct IndexerServiceFFI *indexer, +struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transaction(const struct IndexerServiceFFI *indexer, FfiHashType hash); /** @@ -585,10 +601,8 @@ struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transact * * The caller must ensure that: * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. - * - `runtime` is a valid pointer to a [`Runtime`] instance. */ -struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const struct Runtime *runtime, - const struct IndexerServiceFFI *indexer, +struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const struct IndexerServiceFFI *indexer, struct FfiOption_u64 before, uint64_t limit); @@ -604,16 +618,14 @@ struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const s * * # Returns * - * A `PointerResult, OperationStatus>` indicating success or failure. + * A `PointerResult, OperationStatus>` indicating success or failure. * * # Safety * * The caller must ensure that: * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. - * - `runtime` is a valid pointer to a [`Runtime`] instance. */ -struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transactions_by_account(const struct Runtime *runtime, - const struct IndexerServiceFFI *indexer, +struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transactions_by_account(const struct IndexerServiceFFI *indexer, FfiAccountId account_id, uint64_t offset, uint64_t limit); @@ -621,9 +633,14 @@ struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transaction /** * Frees the resources associated with the given ffi account. * + * Takes ownership of the whole allocation produced by a `query_*` call: the + * outer `Box` (the `PointerResult.value` pointer) *and* its inner + * data buffer. Passing the struct by value previously freed only the inner + * buffer and leaked the outer box. + * * # Arguments * - * - `val`: An instance of `FfiAccount`. + * - `val`: The `*mut FfiAccount` returned in `PointerResult.value`. * * # Returns * @@ -632,12 +649,18 @@ struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transaction * # Safety * * The caller must ensure that: - * - `val` is a valid instance of `FfiAccount`. + * - `val` is a pointer to an `FfiAccount` produced by this library and not yet freed. */ -void free_ffi_account(struct FfiAccount val); +void free_ffi_account(struct FfiAccount *val); /** - * Frees the resources associated with the given ffi block. + * Frees the resources owned by an `FfiBlock` value. + * + * This frees the block's transaction bodies (the only heap-owning field); the + * header/status fields are `Copy`. It operates on the struct by value because + * it is an element-level helper, used both for the vector path + * ([`free_ffi_block_vec`]) and the optional path ([`free_ffi_block_opt`]) — in + * neither case is an `FfiBlock` itself wrapped in its own outer box. * * # Arguments * @@ -650,16 +673,20 @@ void free_ffi_account(struct FfiAccount val); * # Safety * * The caller must ensure that: - * - `val` is a valid instance of `FfiBlock`. + * - `val` is a valid instance of `FfiBlock` produced by this library and not yet freed. */ void free_ffi_block(struct FfiBlock val); /** * Frees the resources associated with the given ffi block option. * + * Takes ownership of the whole allocation produced by a `query_*` call: the + * outer `Box` (the `PointerResult.value` pointer), the inner + * `Box` (when present), and that block's transaction bodies. + * * # Arguments * - * - `val`: An instance of `FfiBlockOpt`. + * - `val`: The `*mut FfiBlockOpt` returned in `PointerResult.value`. * * # Returns * @@ -668,16 +695,20 @@ void free_ffi_block(struct FfiBlock val); * # Safety * * The caller must ensure that: - * - `val` is a valid instance of `FfiBlockOpt`. + * - `val` is a pointer to an `FfiBlockOpt` produced by this library and not yet freed. */ -void free_ffi_block_opt(FfiBlockOpt val); +void free_ffi_block_opt(FfiBlockOpt *val); /** * Frees the resources associated with the given ffi block vector. * + * Takes ownership of the whole allocation produced by a `query_*` call: the + * outer `Box>` (the `PointerResult.value` pointer), the + * vector's backing buffer, and every block within it. + * * # Arguments * - * - `val`: An instance of `FfiVec`. + * - `val`: The `*mut FfiVec` returned in `PointerResult.value`. * * # Returns * @@ -686,9 +717,9 @@ void free_ffi_block_opt(FfiBlockOpt val); * # Safety * * The caller must ensure that: - * - `val` is a valid instance of `FfiVec`. + * - `val` is a pointer to an `FfiVec` produced by this library and not yet freed. */ -void free_ffi_block_vec(struct FfiVec_FfiBlock val); +void free_ffi_block_vec(struct FfiVec_FfiBlock *val); /** * Frees the resources associated with the given ffi transaction. @@ -711,9 +742,13 @@ void free_ffi_transaction(struct FfiTransaction val); /** * Frees the resources associated with the given ffi transaction option. * + * Takes ownership of the whole allocation produced by a `query_*` call: the + * outer `Box>` (the `PointerResult.value` pointer), + * the inner `Box` (when present), and its body. + * * # Arguments * - * - `val`: An instance of `FfiOption`. + * - `val`: The `*mut FfiOption` returned in `PointerResult.value`. * * # Returns * @@ -722,16 +757,21 @@ void free_ffi_transaction(struct FfiTransaction val); * # Safety * * The caller must ensure that: - * - `val` is a valid instance of `FfiOption`. + * - `val` is a pointer to an `FfiOption` produced by this library and not yet + * freed. */ -void free_ffi_transaction_opt(struct FfiOption_FfiTransaction val); +void free_ffi_transaction_opt(struct FfiOption_FfiTransaction *val); /** * Frees the resources associated with the given vector of ffi transactions. * + * Takes ownership of the whole allocation produced by a `query_*` call: the + * outer `Box>` (the `PointerResult.value` pointer), the + * vector's backing buffer, and every transaction within it. + * * # Arguments * - * - `val`: An instance of `FfiVec`. + * - `val`: The `*mut FfiVec` returned in `PointerResult.value`. * * # Returns * @@ -740,9 +780,9 @@ void free_ffi_transaction_opt(struct FfiOption_FfiTransaction val); * # Safety * * The caller must ensure that: - * - `val` is a valid instance of `FfiVec`. + * - `val` is a pointer to an `FfiVec` produced by this library and not yet freed. */ -void free_ffi_transaction_vec(struct FfiVec_FfiTransaction val); +void free_ffi_transaction_vec(struct FfiVec_FfiTransaction *val); bool is_ok(const enum OperationStatus *self); diff --git a/lez/indexer/ffi/src/api/client.rs b/lez/indexer/ffi/src/api/client.rs deleted file mode 100644 index 825a57de..00000000 --- a/lez/indexer/ffi/src/api/client.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::net::SocketAddr; - -use url::Url; - -use crate::OperationStatus; - -#[derive(Debug, Clone, Copy)] -pub enum UrlProtocol { - Http, - Ws, -} - -impl std::fmt::Display for UrlProtocol { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Http => write!(f, "http"), - Self::Ws => write!(f, "ws"), - } - } -} - -pub(crate) fn addr_to_url(protocol: UrlProtocol, addr: SocketAddr) -> Result { - // Convert 0.0.0.0 to 127.0.0.1 for client connections - // When binding to port 0, the server binds to 0.0.0.0: - // but clients need to connect to 127.0.0.1: to work reliably - let url_string = if addr.ip().is_unspecified() { - format!("{protocol}://127.0.0.1:{}", addr.port()) - } else { - format!("{protocol}://{addr}") - }; - - url_string.parse().map_err(|e| { - log::error!("Could not parse indexer url: {e}"); - OperationStatus::InitializationError - }) -} diff --git a/lez/indexer/ffi/src/api/lifecycle.rs b/lez/indexer/ffi/src/api/lifecycle.rs index d124901f..bedebb2b 100644 --- a/lez/indexer/ffi/src/api/lifecycle.rs +++ b/lez/indexer/ffi/src/api/lifecycle.rs @@ -1,14 +1,9 @@ use std::{ffi::c_char, path::PathBuf}; -use crate::{ - IndexerServiceFFI, Runtime, - api::{ - PointerResult, - client::{UrlProtocol, addr_to_url}, - }, - client::{IndexerClient, IndexerClientTrait as _}, - errors::OperationStatus, -}; +use futures::StreamExt as _; +use indexer_core::{IndexerCore, config::IndexerConfig}; + +use crate::{IndexerServiceFFI, Runtime, api::PointerResult, errors::OperationStatus}; pub type InitializedIndexerServiceFFIResult = PointerResult; @@ -18,7 +13,6 @@ pub type InitializedIndexerServiceFFIResult = PointerResult InitializedIndexerServiceFFIResult { // SAFETY: The caller must ensure the validness of the `runtime` and `config_path` pointers. - unsafe { setup_indexer(runtime, config_path, port) }.map_or_else( + unsafe { setup_indexer(runtime, config_path) }.map_or_else( InitializedIndexerServiceFFIResult::from_error, InitializedIndexerServiceFFIResult::from_value, ) @@ -57,7 +50,6 @@ pub extern "C" fn new_runtime() -> PointerResult { /// # Arguments /// /// - `config_path`: A pointer to a string representing the path to the configuration file. -/// - `port`: Number representing a port, on which indexers RPC will start. /// /// # Returns /// @@ -71,7 +63,6 @@ pub extern "C" fn new_runtime() -> PointerResult { unsafe fn setup_indexer( runtime: *const Runtime, config_path: *const c_char, - port: u16, ) -> Result { let user_config_path = PathBuf::from( unsafe { std::ffi::CStr::from_ptr(config_path) } @@ -81,7 +72,7 @@ unsafe fn setup_indexer( OperationStatus::InitializationError })?, ); - let config = indexer_service::IndexerConfig::from_path(&user_config_path).map_err(|e| { + let config = IndexerConfig::from_path(&user_config_path).map_err(|e| { log::error!("Failed to read config: {e}"); OperationStatus::InitializationError })?; @@ -90,22 +81,30 @@ unsafe fn setup_indexer( // `tokio::runtime::Runtime` instance. let runtime = unsafe { &*runtime }; - let indexer_handle = runtime - .block_on(indexer_service::run_server(config, port)) - .map_err(|e| { - log::error!("Could not start indexer service: {e}"); - OperationStatus::InitializationError - })?; + let core = IndexerCore::new(config).map_err(|e| { + log::error!("Could not initialize indexer core: {e}"); + OperationStatus::InitializationError + })?; - let indexer_url = addr_to_url(UrlProtocol::Ws, indexer_handle.addr())?; - let indexer_client = runtime - .block_on(IndexerClient::new(&indexer_url)) - .map_err(|e| { - log::error!("Could not start indexer client: {e}"); - OperationStatus::InitializationError - })?; + // The block stream writes each parsed block into the store as a side effect + // of being polled, so we spawn a task that simply drains it. There are no + // subscribers — queries read the store directly via `core()`. + let ingest_core = core.clone(); + let ingest_handle = runtime.spawn(async move { + let mut block_stream = std::pin::pin!(ingest_core.subscribe_parse_block_stream()); + while let Some(result) = block_stream.next().await { + if let Err(e) = result { + log::error!("Indexer ingestion error: {e:#}"); + } + } + log::warn!("Indexer block stream ended"); + }); - Ok(IndexerServiceFFI::new(indexer_handle, indexer_client)) + Ok(IndexerServiceFFI::new( + core, + ingest_handle, + runtime.handle().clone(), + )) } /// Stops and frees the resources associated with the given indexer service. diff --git a/lez/indexer/ffi/src/api/logging.rs b/lez/indexer/ffi/src/api/logging.rs new file mode 100644 index 00000000..207d4b61 --- /dev/null +++ b/lez/indexer/ffi/src/api/logging.rs @@ -0,0 +1,14 @@ +/// Initializes the FFI's logger. +/// +/// Wires up `env_logger`, so the library's `log::*` output is controlled by the +/// `RUST_LOG` environment variable (e.g. `RUST_LOG=info`). Without this, the +/// FFI's log calls go nowhere — and since failures are otherwise reported only +/// as numeric [`OperationStatus`](crate::errors::OperationStatus) codes, there +/// is no other way to see *why* a call failed. +/// +/// Safe to call multiple times and from any consumer: if a global logger is +/// already set, the call is a no-op. +#[unsafe(no_mangle)] +pub extern "C" fn init_logger() { + let _ignore_me = env_logger::try_init(); +} diff --git a/lez/indexer/ffi/src/api/mod.rs b/lez/indexer/ffi/src/api/mod.rs index ea2b91d7..2e4be797 100644 --- a/lez/indexer/ffi/src/api/mod.rs +++ b/lez/indexer/ffi/src/api/mod.rs @@ -1,7 +1,7 @@ pub use result::PointerResult; -pub mod client; pub mod lifecycle; +pub mod logging; pub mod memory; pub mod query; pub mod result; diff --git a/lez/indexer/ffi/src/api/query.rs b/lez/indexer/ffi/src/api/query.rs index f10de598..548deabc 100644 --- a/lez/indexer/ffi/src/api/query.rs +++ b/lez/indexer/ffi/src/api/query.rs @@ -1,8 +1,7 @@ -use indexer_service_protocol::{AccountId, HashType}; -use indexer_service_rpc::RpcClient as _; +use indexer_service_protocol::AccountId; use crate::{ - IndexerServiceFFI, Runtime, + IndexerServiceFFI, api::{ PointerResult, types::{ @@ -15,6 +14,45 @@ use crate::{ errors::OperationStatus, }; +/// Result of [`query_last_block`], returned **inline** (no heap allocation, so +/// there is no corresponding `free_*` to call). +/// +/// `block_id` is only meaningful when `error` is `Ok` *and* `is_some` is +/// `true`. An `Ok` result with `is_some == false` means the indexer has no +/// finalized block yet (an empty chain) — which is distinct from an error. +#[repr(C)] +pub struct LastBlockIdResult { + pub block_id: u64, + pub is_some: bool, + pub error: OperationStatus, +} + +impl LastBlockIdResult { + const fn error(error: OperationStatus) -> Self { + Self { + block_id: 0, + is_some: false, + error, + } + } + + const fn none() -> Self { + Self { + block_id: 0, + is_some: false, + error: OperationStatus::Ok, + } + } + + const fn some(block_id: u64) -> Self { + Self { + block_id, + is_some: true, + error: OperationStatus::Ok, + } + } +} + /// Query the last block id from indexer. /// /// # Arguments @@ -23,34 +61,29 @@ use crate::{ /// /// # Returns /// -/// A `PointerResult, OperationStatus>` indicating success or failure. +/// A [`LastBlockIdResult`] indicating success or failure. The block id is +/// returned inline; nothing needs to be freed. /// /// # Safety /// /// The caller must ensure that: -/// - `runtime` is a valid pointer to a [`Runtime`] instance. /// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. #[unsafe(no_mangle)] -pub unsafe extern "C" fn query_last_block( - runtime: *const Runtime, - indexer: *const IndexerServiceFFI, -) -> PointerResult, OperationStatus> { +pub unsafe extern "C" fn query_last_block(indexer: *const IndexerServiceFFI) -> LastBlockIdResult { if indexer.is_null() { log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting."); - return PointerResult::from_error(OperationStatus::NullPointer); + return LastBlockIdResult::error(OperationStatus::NullPointer); } let indexer = unsafe { &*indexer }; - let client = indexer.client(); - let runtime = unsafe { &*runtime }; - - runtime - .block_on(client.get_last_finalized_block_id()) - .map_or_else( - |_| PointerResult::from_error(OperationStatus::ClientError), - PointerResult::from_value, - ) + indexer.core().store.get_last_block_id().map_or_else( + |e| { + log::error!("Failed to query last block id: {e:#}"); + LastBlockIdResult::error(OperationStatus::ClientError) + }, + |opt| opt.map_or_else(LastBlockIdResult::none, LastBlockIdResult::some), + ) } /// Query the block by id from indexer. @@ -67,11 +100,9 @@ pub unsafe extern "C" fn query_last_block( /// # Safety /// /// The caller must ensure that: -/// - `runtime` is a valid pointer to a [`Runtime`] instance. /// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. #[unsafe(no_mangle)] pub unsafe extern "C" fn query_block( - runtime: *const Runtime, indexer: *const IndexerServiceFFI, block_id: FfiBlockId, ) -> PointerResult { @@ -82,24 +113,23 @@ pub unsafe extern "C" fn query_block( let indexer = unsafe { &*indexer }; - let client = indexer.client(); - let runtime = unsafe { &*runtime }; + indexer.core().store.get_block_at_id(block_id).map_or_else( + |e| { + log::error!("Failed to query block by id: {e:#}"); + PointerResult::from_error(OperationStatus::ClientError) + }, + |block_opt| { + let block_ffi = block_opt.map_or_else(FfiBlockOpt::from_none, |block| { + let block: indexer_service_protocol::Block = block.into(); + FfiBlockOpt::from_value(block.into()) + }); - runtime - .block_on(client.get_block_by_id(block_id)) - .map_or_else( - |_| PointerResult::from_error(OperationStatus::ClientError), - |block_opt| { - let block_ffi = block_opt.map_or_else(FfiBlockOpt::from_none, |block| { - FfiBlockOpt::from_value(block.into()) - }); - - PointerResult::from_value(block_ffi) - }, - ) + PointerResult::from_value(block_ffi) + }, + ) } -/// Query the block by id from indexer. +/// Query the block by hash from indexer. /// /// # Arguments /// @@ -113,11 +143,9 @@ pub unsafe extern "C" fn query_block( /// # Safety /// /// The caller must ensure that: -/// - `runtime` is a valid pointer to a [`Runtime`] instance. /// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. #[unsafe(no_mangle)] pub unsafe extern "C" fn query_block_by_hash( - runtime: *const Runtime, indexer: *const IndexerServiceFFI, hash: FfiHashType, ) -> PointerResult { @@ -128,15 +156,18 @@ pub unsafe extern "C" fn query_block_by_hash( let indexer = unsafe { &*indexer }; - let client = indexer.client(); - let runtime = unsafe { &*runtime }; - - runtime - .block_on(client.get_block_by_hash(HashType(hash.data))) + indexer + .core() + .store + .get_block_by_hash(hash.data) .map_or_else( - |_| PointerResult::from_error(OperationStatus::ClientError), + |e| { + log::error!("Failed to query block by hash: {e:#}"); + PointerResult::from_error(OperationStatus::ClientError) + }, |block_opt| { let block_ffi = block_opt.map_or_else(FfiBlockOpt::from_none, |block| { + let block: indexer_service_protocol::Block = block.into(); FfiBlockOpt::from_value(block.into()) }); @@ -159,11 +190,9 @@ pub unsafe extern "C" fn query_block_by_hash( /// # Safety /// /// The caller must ensure that: -/// - `runtime` is a valid pointer to a [`Runtime`] instance. /// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. #[unsafe(no_mangle)] pub unsafe extern "C" fn query_account( - runtime: *const Runtime, indexer: *const IndexerServiceFFI, account_id: FfiAccountId, ) -> PointerResult { @@ -174,23 +203,29 @@ pub unsafe extern "C" fn query_account( let indexer = unsafe { &*indexer }; - let client = indexer.client(); - let runtime = unsafe { &*runtime }; - - runtime - .block_on(client.get_account(AccountId { - value: account_id.data, - })) + // `account_current_state` is the only async store call; drive it on the + // runtime the indexer was started on. + let account_id = AccountId { + value: account_id.data, + }; + indexer + .runtime_handle() + .block_on( + indexer + .core() + .store + .account_current_state(&account_id.into()), + ) .map_or_else( - |_| PointerResult::from_error(OperationStatus::ClientError), - |acc| { - let acc_lee: lee::Account = acc.try_into().expect("Source is in blocks, must fit"); - PointerResult::from_value(acc_lee.into()) + |e| { + log::error!("Failed to query account: {e:#}"); + PointerResult::from_error(OperationStatus::ClientError) }, + |account| PointerResult::from_value(account.into()), ) } -/// Query the trasnaction by hash from indexer. +/// Query the transaction by hash from indexer. /// /// # Arguments /// @@ -205,10 +240,8 @@ pub unsafe extern "C" fn query_account( /// /// The caller must ensure that: /// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. -/// - `runtime` is a valid pointer to a [`Runtime`] instance. #[unsafe(no_mangle)] pub unsafe extern "C" fn query_transaction( - runtime: *const Runtime, indexer: *const IndexerServiceFFI, hash: FfiHashType, ) -> PointerResult, OperationStatus> { @@ -219,15 +252,18 @@ pub unsafe extern "C" fn query_transaction( let indexer = unsafe { &*indexer }; - let client = indexer.client(); - let runtime = unsafe { &*runtime }; - - runtime - .block_on(client.get_transaction(HashType(hash.data))) + indexer + .core() + .store + .get_transaction_by_hash(hash.data) .map_or_else( - |_| PointerResult::from_error(OperationStatus::ClientError), + |e| { + log::error!("Failed to query transaction: {e:#}"); + PointerResult::from_error(OperationStatus::ClientError) + }, |tx_opt| { let tx_ffi = tx_opt.map_or_else(FfiOption::::from_none, |tx| { + let tx: indexer_service_protocol::Transaction = tx.into(); FfiOption::::from_value(tx.into()) }); @@ -252,10 +288,8 @@ pub unsafe extern "C" fn query_transaction( /// /// The caller must ensure that: /// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. -/// - `runtime` is a valid pointer to a [`Runtime`] instance. #[unsafe(no_mangle)] pub unsafe extern "C" fn query_block_vec( - runtime: *const Runtime, indexer: *const IndexerServiceFFI, before: FfiOption, limit: u64, @@ -267,21 +301,26 @@ pub unsafe extern "C" fn query_block_vec( let indexer = unsafe { &*indexer }; - let client = indexer.client(); - let runtime = unsafe { &*runtime }; - let before_std = before.is_some.then(|| unsafe { *before.value }); - runtime - .block_on(client.get_blocks(before_std, limit)) + indexer + .core() + .store + .get_block_batch(before_std, limit) .map_or_else( - |_| PointerResult::from_error(OperationStatus::ClientError), + |e| { + log::error!("Failed to query block batch: {e:#}"); + PointerResult::from_error(OperationStatus::ClientError) + }, |block_vec| { PointerResult::from_value( block_vec .into_iter() - .map(Into::into) - .collect::>() + .map(|block| { + let block: indexer_service_protocol::Block = block.into(); + block.into() + }) + .collect::>() .into(), ) }, @@ -299,16 +338,14 @@ pub unsafe extern "C" fn query_block_vec( /// /// # Returns /// -/// A `PointerResult, OperationStatus>` indicating success or failure. +/// A `PointerResult, OperationStatus>` indicating success or failure. /// /// # Safety /// /// The caller must ensure that: /// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. -/// - `runtime` is a valid pointer to a [`Runtime`] instance. #[unsafe(no_mangle)] pub unsafe extern "C" fn query_transactions_by_account( - runtime: *const Runtime, indexer: *const IndexerServiceFFI, account_id: FfiAccountId, offset: u64, @@ -321,25 +358,24 @@ pub unsafe extern "C" fn query_transactions_by_account( let indexer = unsafe { &*indexer }; - let client = indexer.client(); - let runtime = unsafe { &*runtime }; - - runtime - .block_on(client.get_transactions_by_account( - AccountId { - value: account_id.data, - }, - offset, - limit, - )) + indexer + .core() + .store + .get_transactions_by_account(account_id.data, offset, limit) .map_or_else( - |_| PointerResult::from_error(OperationStatus::ClientError), + |e| { + log::error!("Failed to query transactions by account: {e:#}"); + PointerResult::from_error(OperationStatus::ClientError) + }, |tx_vec| { PointerResult::from_value( tx_vec .into_iter() - .map(Into::into) - .collect::>() + .map(|tx| { + let tx: indexer_service_protocol::Transaction = tx.into(); + tx.into() + }) + .collect::>() .into(), ) }, diff --git a/lez/indexer/ffi/src/api/types/account.rs b/lez/indexer/ffi/src/api/types/account.rs index 2309b84b..f2eb8e58 100644 --- a/lez/indexer/ffi/src/api/types/account.rs +++ b/lez/indexer/ffi/src/api/types/account.rs @@ -100,9 +100,14 @@ impl From<&FfiAccount> for indexer_service_protocol::Account { /// Frees the resources associated with the given ffi account. /// +/// Takes ownership of the whole allocation produced by a `query_*` call: the +/// outer `Box` (the `PointerResult.value` pointer) *and* its inner +/// data buffer. Passing the struct by value previously freed only the inner +/// buffer and leaked the outer box. +/// /// # Arguments /// -/// - `val`: An instance of `FfiAccount`. +/// - `val`: The `*mut FfiAccount` returned in `PointerResult.value`. /// /// # Returns /// @@ -111,9 +116,15 @@ impl From<&FfiAccount> for indexer_service_protocol::Account { /// # Safety /// /// The caller must ensure that: -/// - `val` is a valid instance of `FfiAccount`. +/// - `val` is a pointer to an `FfiAccount` produced by this library and not yet freed. #[unsafe(no_mangle)] -pub unsafe extern "C" fn free_ffi_account(val: FfiAccount) { - let orig_val: indexer_service_protocol::Account = val.into(); +pub unsafe extern "C" fn free_ffi_account(val: *mut FfiAccount) { + if val.is_null() { + log::error!("Trying to free a null pointer. Exiting"); + return; + } + // Reclaim the outer box, then convert to drop the inner data buffer. + let boxed = unsafe { Box::from_raw(val) }; + let orig_val: indexer_service_protocol::Account = (*boxed).into(); drop(orig_val); } diff --git a/lez/indexer/ffi/src/api/types/block.rs b/lez/indexer/ffi/src/api/types/block.rs index e7ae0760..b652a7fe 100644 --- a/lez/indexer/ffi/src/api/types/block.rs +++ b/lez/indexer/ffi/src/api/types/block.rs @@ -2,7 +2,7 @@ use indexer_service_protocol::{BedrockStatus, Block, BlockHeader, HashType, Sign use crate::api::types::{ FfiBlockId, FfiHashType, FfiOption, FfiSignature, FfiTimestamp, FfiVec, - transaction::free_ffi_transaction_vec, vectors::FfiBlockBody, + transaction::free_transaction_vec_value, vectors::FfiBlockBody, }; #[repr(C)] @@ -91,7 +91,13 @@ impl From for BedrockStatus { } } -/// Frees the resources associated with the given ffi block. +/// Frees the resources owned by an `FfiBlock` value. +/// +/// This frees the block's transaction bodies (the only heap-owning field); the +/// header/status fields are `Copy`. It operates on the struct by value because +/// it is an element-level helper, used both for the vector path +/// ([`free_ffi_block_vec`]) and the optional path ([`free_ffi_block_opt`]) — in +/// neither case is an `FfiBlock` itself wrapped in its own outer box. /// /// # Arguments /// @@ -104,7 +110,7 @@ impl From for BedrockStatus { /// # Safety /// /// The caller must ensure that: -/// - `val` is a valid instance of `FfiBlock`. +/// - `val` is a valid instance of `FfiBlock` produced by this library and not yet freed. #[unsafe(no_mangle)] pub unsafe extern "C" fn free_ffi_block(val: FfiBlock) { // We don't really need all the casts, but just in case @@ -121,16 +127,18 @@ pub unsafe extern "C" fn free_ffi_block(val: FfiBlock) { #[expect(clippy::let_underscore_must_use, reason = "No use for this Copy type")] let _: BedrockStatus = val.bedrock_status.into(); - unsafe { - free_ffi_transaction_vec(ffi_tx_ffi_vec); - }; + free_transaction_vec_value(ffi_tx_ffi_vec); } /// Frees the resources associated with the given ffi block option. /// +/// Takes ownership of the whole allocation produced by a `query_*` call: the +/// outer `Box` (the `PointerResult.value` pointer), the inner +/// `Box` (when present), and that block's transaction bodies. +/// /// # Arguments /// -/// - `val`: An instance of `FfiBlockOpt`. +/// - `val`: The `*mut FfiBlockOpt` returned in `PointerResult.value`. /// /// # Returns /// @@ -139,37 +147,32 @@ pub unsafe extern "C" fn free_ffi_block(val: FfiBlock) { /// # Safety /// /// The caller must ensure that: -/// - `val` is a valid instance of `FfiBlockOpt`. +/// - `val` is a pointer to an `FfiBlockOpt` produced by this library and not yet freed. #[unsafe(no_mangle)] -pub unsafe extern "C" fn free_ffi_block_opt(val: FfiBlockOpt) { - if val.is_some { - let value = unsafe { Box::from_raw(val.value) }; - - // We don't really need all the casts, but just in case - // All except `ffi_tx_ffi_vec` is Copy types, so no need for Drop - let _ = BlockHeader { - block_id: value.header.block_id, - prev_block_hash: HashType(value.header.prev_block_hash.data), - hash: HashType(value.header.hash.data), - timestamp: value.header.timestamp, - signature: Signature(value.header.signature.data), - }; - let ffi_tx_ffi_vec = value.body; - - #[expect(clippy::let_underscore_must_use, reason = "No use for this Copy type")] - let _: BedrockStatus = value.bedrock_status.into(); - +pub unsafe extern "C" fn free_ffi_block_opt(val: *mut FfiBlockOpt) { + if val.is_null() { + log::error!("Trying to free a null pointer. Exiting"); + return; + } + // Reclaim the outer box, then the inner block box (if any). + let opt = unsafe { Box::from_raw(val) }; + if opt.is_some { + let block = unsafe { Box::from_raw(opt.value) }; unsafe { - free_ffi_transaction_vec(ffi_tx_ffi_vec); - }; + free_ffi_block(*block); + } } } /// Frees the resources associated with the given ffi block vector. /// +/// Takes ownership of the whole allocation produced by a `query_*` call: the +/// outer `Box>` (the `PointerResult.value` pointer), the +/// vector's backing buffer, and every block within it. +/// /// # Arguments /// -/// - `val`: An instance of `FfiVec`. +/// - `val`: The `*mut FfiVec` returned in `PointerResult.value`. /// /// # Returns /// @@ -178,10 +181,16 @@ pub unsafe extern "C" fn free_ffi_block_opt(val: FfiBlockOpt) { /// # Safety /// /// The caller must ensure that: -/// - `val` is a valid instance of `FfiVec`. +/// - `val` is a pointer to an `FfiVec` produced by this library and not yet freed. #[unsafe(no_mangle)] -pub unsafe extern "C" fn free_ffi_block_vec(val: FfiVec) { - let ffi_block_std_vec: Vec<_> = val.into(); +pub unsafe extern "C" fn free_ffi_block_vec(val: *mut FfiVec) { + if val.is_null() { + log::error!("Trying to free a null pointer. Exiting"); + return; + } + // Reclaim the outer box, then the backing buffer and each block. + let boxed = unsafe { Box::from_raw(val) }; + let ffi_block_std_vec: Vec<_> = (*boxed).into(); for block in ffi_block_std_vec { unsafe { free_ffi_block(block); diff --git a/lez/indexer/ffi/src/api/types/transaction.rs b/lez/indexer/ffi/src/api/types/transaction.rs index ca733ed3..d5cb9035 100644 --- a/lez/indexer/ffi/src/api/types/transaction.rs +++ b/lez/indexer/ffi/src/api/types/transaction.rs @@ -463,9 +463,13 @@ pub unsafe extern "C" fn free_ffi_transaction(val: FfiTransaction) { /// Frees the resources associated with the given ffi transaction option. /// +/// Takes ownership of the whole allocation produced by a `query_*` call: the +/// outer `Box>` (the `PointerResult.value` pointer), +/// the inner `Box` (when present), and its body. +/// /// # Arguments /// -/// - `val`: An instance of `FfiOption`. +/// - `val`: The `*mut FfiOption` returned in `PointerResult.value`. /// /// # Returns /// @@ -474,48 +478,32 @@ pub unsafe extern "C" fn free_ffi_transaction(val: FfiTransaction) { /// # Safety /// /// The caller must ensure that: -/// - `val` is a valid instance of `FfiOption`. +/// - `val` is a pointer to an `FfiOption` produced by this library and not yet +/// freed. #[unsafe(no_mangle)] -pub unsafe extern "C" fn free_ffi_transaction_opt(val: FfiOption) { - if val.is_some { - let value = unsafe { Box::from_raw(val.value) }; - - match value.kind { - FfiTransactionKind::Public => { - let body = unsafe { Box::from_raw(value.body.public_body) }; - let std_body: PublicTransaction = body.into(); - drop(std_body); - } - FfiTransactionKind::Private => { - let body = unsafe { Box::from_raw(value.body.private_body) }; - let std_body: PrivacyPreservingTransaction = body.into(); - drop(std_body); - } - FfiTransactionKind::ProgramDeploy => { - let body = unsafe { Box::from_raw(value.body.program_deployment_body) }; - let std_body: ProgramDeploymentTransaction = body.into(); - drop(std_body); - } +pub unsafe extern "C" fn free_ffi_transaction_opt(val: *mut FfiOption) { + if val.is_null() { + log::error!("Trying to free a null pointer. Exiting"); + return; + } + // Reclaim the outer box, then the inner transaction box (if any). + let opt = unsafe { Box::from_raw(val) }; + if opt.is_some { + let tx = unsafe { Box::from_raw(opt.value) }; + unsafe { + free_ffi_transaction(*tx); } } } -/// Frees the resources associated with the given vector of ffi transactions. +/// Frees the resources owned by an `FfiVec` value (the backing +/// buffer and each transaction), without owning an outer box. /// -/// # Arguments -/// -/// - `val`: An instance of `FfiVec`. -/// -/// # Returns -/// -/// void. -/// -/// # Safety -/// -/// The caller must ensure that: -/// - `val` is a valid instance of `FfiVec`. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn free_ffi_transaction_vec(val: FfiVec) { +/// This is the element-level helper shared by the block free path +/// ([`crate::api::types::block::free_ffi_block`], whose body is a transaction +/// vector held by value) and the public [`free_ffi_transaction_vec`] entry +/// point (which first reclaims the outer box). +pub(crate) fn free_transaction_vec_value(val: FfiVec) { let ffi_tx_std_vec: Vec<_> = val.into(); for tx in ffi_tx_std_vec { unsafe { @@ -524,6 +512,35 @@ pub unsafe extern "C" fn free_ffi_transaction_vec(val: FfiVec) { } } +/// Frees the resources associated with the given vector of ffi transactions. +/// +/// Takes ownership of the whole allocation produced by a `query_*` call: the +/// outer `Box>` (the `PointerResult.value` pointer), the +/// vector's backing buffer, and every transaction within it. +/// +/// # Arguments +/// +/// - `val`: The `*mut FfiVec` returned in `PointerResult.value`. +/// +/// # Returns +/// +/// void. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `val` is a pointer to an `FfiVec` produced by this library and not yet freed. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_ffi_transaction_vec(val: *mut FfiVec) { + if val.is_null() { + log::error!("Trying to free a null pointer. Exiting"); + return; + } + // Reclaim the outer box, then the backing buffer and each transaction. + let boxed = unsafe { Box::from_raw(val) }; + free_transaction_vec_value(*boxed); +} + fn cast_validity_window(window: ValidityWindow) -> [u64; 2] { [ window.0.0.unwrap_or_default(), diff --git a/lez/indexer/ffi/src/client.rs b/lez/indexer/ffi/src/client.rs deleted file mode 100644 index f05b350e..00000000 --- a/lez/indexer/ffi/src/client.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::{ops::Deref, sync::Arc}; - -use anyhow::{Context as _, Result}; -use log::info; -pub use url::Url; - -pub trait IndexerClientTrait: Clone { - async fn new(indexer_url: &Url) -> Result; -} - -#[derive(Clone)] -pub struct IndexerClient(Arc); - -impl IndexerClientTrait for IndexerClient { - async fn new(indexer_url: &Url) -> Result { - info!("Connecting to Indexer at {indexer_url}"); - - let client = jsonrpsee::ws_client::WsClientBuilder::default() - .build(indexer_url) - .await - .context("Failed to create websocket client")?; - - Ok(Self(Arc::new(client))) - } -} - -impl Deref for IndexerClient { - type Target = jsonrpsee::ws_client::WsClient; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} diff --git a/lez/indexer/ffi/src/indexer.rs b/lez/indexer/ffi/src/indexer.rs index e8707697..327773b3 100644 --- a/lez/indexer/ffi/src/indexer.rs +++ b/lez/indexer/ffi/src/indexer.rs @@ -1,95 +1,75 @@ -use std::{ffi::c_void, net::SocketAddr}; +use std::ffi::c_void; -use indexer_service::IndexerHandle; - -use crate::client::IndexerClient; +use indexer_core::IndexerCore; +use tokio::{runtime::Handle, task::JoinHandle}; +/// FFI-owned indexer. +/// +/// Has three fields behind `c_void` (so that cbindgen never needs to see their Rust layout): +/// - An [`IndexerCore`] used to answer queries +/// - The background task [`JoinHandle`] that drives ingestion (consuming the block stream so the +/// store stays populated) +/// - A [`Handle`] to the runtime they live on. #[repr(C)] pub struct IndexerServiceFFI { - indexer_handle: *mut c_void, - indexer_client: *mut c_void, + core: *mut c_void, + ingest_handle: *mut c_void, + runtime_handle: *mut c_void, } impl IndexerServiceFFI { #[must_use] - pub fn new( - indexer_handle: indexer_service::IndexerHandle, - indexer_client: IndexerClient, - ) -> Self { + pub fn new(core: IndexerCore, ingest_handle: JoinHandle<()>, runtime_handle: Handle) -> Self { Self { - // Box the complex types and convert to opaque pointers - indexer_handle: Box::into_raw(Box::new(indexer_handle)).cast::(), - indexer_client: Box::into_raw(Box::new(indexer_client)).cast::(), + core: Box::into_raw(Box::new(core)).cast::(), + ingest_handle: Box::into_raw(Box::new(ingest_handle)).cast::(), + runtime_handle: Box::into_raw(Box::new(runtime_handle)).cast::(), } } - /// Helper to take ownership back. + /// Borrow the [`IndexerCore`] to run a query against its store. #[must_use] - pub fn into_parts(mut self) -> (Box, Box) { - let Self { - indexer_handle, - indexer_client, - } = &mut self; - - let indexer_handle_boxed = unsafe { Box::from_raw(indexer_handle.cast::()) }; - let indexer_client_boxed = unsafe { Box::from_raw(indexer_client.cast::()) }; - - // Assigning nulls to prevent double free on drop, since ownership is transferred to caller - *indexer_handle = std::ptr::null_mut(); - *indexer_client = std::ptr::null_mut(); - - (indexer_handle_boxed, indexer_client_boxed) - } - - /// Helper to get indexer handle addr. - #[must_use] - pub const fn addr(&self) -> SocketAddr { - let indexer_handle = unsafe { - self.indexer_handle - .cast::() - .as_ref() - .expect("Indexer Handle must be non-null pointer") - }; - - indexer_handle.addr() - } - - /// Helper to get indexer handle ref. - #[must_use] - pub const fn handle(&self) -> &IndexerHandle { + pub const fn core(&self) -> &IndexerCore { unsafe { - self.indexer_handle - .cast::() + self.core + .cast::() .as_ref() - .expect("Indexer Handle must be non-null pointer") + .expect("IndexerCore must be a non-null pointer") } } - /// Helper to get indexer client ref. + /// Borrow the runtime handle to `block_on` an async store query. #[must_use] - pub const fn client(&self) -> &IndexerClient { + pub const fn runtime_handle(&self) -> &Handle { unsafe { - self.indexer_client - .cast::() + self.runtime_handle + .cast::() .as_ref() - .expect("Indexer Client must be non-null pointer") + .expect("Runtime handle must be a non-null pointer") } } } -// Implement Drop to prevent memory leaks +// Implement Drop to stop ingestion and free the boxed resources. impl Drop for IndexerServiceFFI { fn drop(&mut self) { let Self { - indexer_handle, - indexer_client, + core, + ingest_handle, + runtime_handle, } = self; - if !indexer_handle.is_null() { - drop(unsafe { Box::from_raw(indexer_handle.cast::()) }); + if !ingest_handle.is_null() { + // Stop the background ingestion task before tearing down the core. + let handle = unsafe { Box::from_raw(ingest_handle.cast::>()) }; + handle.abort(); + drop(handle); } - if !indexer_client.is_null() { - drop(unsafe { Box::from_raw(indexer_client.cast::()) }); + if !core.is_null() { + drop(unsafe { Box::from_raw(core.cast::()) }); + } + if !runtime_handle.is_null() { + drop(unsafe { Box::from_raw(runtime_handle.cast::()) }); } } } diff --git a/lez/indexer/ffi/src/lib.rs b/lez/indexer/ffi/src/lib.rs index 9e34b111..0ca197c7 100644 --- a/lez/indexer/ffi/src/lib.rs +++ b/lez/indexer/ffi/src/lib.rs @@ -5,7 +5,6 @@ pub use indexer::IndexerServiceFFI; pub use runtime::Runtime; pub mod api; -mod client; mod errors; mod indexer; mod runtime;