diff --git a/README.md b/README.md index 420f92f..4910e6a 100644 --- a/README.md +++ b/README.md @@ -40,11 +40,11 @@ On success it returns `0`; a non-zero return is the FFI `OperationStatus` (e.g. > [!TIP] > -> By default the indexer is silent. Call `init_logger(level)` once (e.g. -> `init_logger("info")`; accepts `off`/`error`/`warn`/`info`/`debug`/`trace`) to -> surface the indexer's own `log` output — useful since a failed call otherwise -> only reports a numeric `OperationStatus`. Logging is scoped to the indexer -> crates, and the first call wins. +> The module logs its lifecycle (start/stop, the resolved config + storage paths, +> and failures with their `OperationStatus`) to stderr, which the `logos_host` +> captures and surfaces through its own logger. For ongoing indexer health — sync +> state, and a parked/stalled tip with its reason — poll `getStatus()`; that runs +> inside the indexer and reports what the C++ lifecycle logs can't see. ### Query methods (the Logos-protocol API) diff --git a/config/indexer_config.json b/config/indexer_config.json index 72ec53f..af991bb 100644 --- a/config/indexer_config.json +++ b/config/indexer_config.json @@ -7,47 +7,5 @@ "max_retries": 5 } }, - "channel_id": "0101010101010101010101010101010101010101010101010101010101010101", - "initial_accounts": [ - { - "account_id": "CbgR6tj5kWx5oziiFptM7jMvrQeYY3Mzaao6ciuhSr2r", - "balance": 10000 - }, - { - "account_id": "2RHZhw9h534Zr3eq2RGhQete2Hh667foECzXPmSkGni2", - "balance": 20000 - } - ], - "initial_commitments": [ - { - "npk": [ - 139, 19, 158, 11, 155, 231, 85, 206, 132, 228, 220, 114, 145, 89, 113, - 156, 238, 142, 242, 74, 182, 91, 43, 100, 6, 190, 31, 15, 31, 88, 96, - 204 - ], - "account": { - "program_owner": [0, 0, 0, 0, 0, 0, 0, 0], - "balance": 10000, - "data": [], - "nonce": 0 - } - }, - { - "npk": [ - 173, 134, 33, 223, 54, 226, 10, 71, 215, 254, 143, 172, 24, 244, 243, - 208, 65, 112, 118, 70, 217, 240, 69, 100, 129, 3, 121, 25, 213, 132, 42, - 45 - ], - "account": { - "program_owner": [0, 0, 0, 0, 0, 0, 0, 0], - "balance": 20000, - "data": [], - "nonce": 0 - } - } - ], - "signing_key": [ - 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, - 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37 - ] + "channel_id": "0101010101010101010101010101010101010101010101010101010101010101" } diff --git a/flake.lock b/flake.lock index fb76b21..7ff4e6d 100644 --- a/flake.lock +++ b/flake.lock @@ -1574,16 +1574,16 @@ "rust-rapidsnark": "rust-rapidsnark" }, "locked": { - "lastModified": 1782820738, - "narHash": "sha256-OnXBx3nD/r7vCzZwh/RXmoqbtNF9rG+ZZsWXPsXoOzk=", - "ref": "refs/tags/v0.2.0", - "rev": "a58fbce2ff48c58b7bb5001b1a27e64b9596ee3a", - "revCount": 2828, + "lastModified": 1782941647, + "narHash": "sha256-H7T0mBWY75tZjCf9zcQUAFteurZhqnZ+2srtdPeQTRQ=", + "ref": "main", + "rev": "d1637b65e2479e80bb617b900ec5d4024216457d", + "revCount": 2830, "type": "git", "url": "https://github.com/logos-blockchain/logos-execution-zone" }, "original": { - "ref": "refs/tags/v0.2.0", + "ref": "main", "type": "git", "url": "https://github.com/logos-blockchain/logos-execution-zone" } diff --git a/flake.nix b/flake.nix index bf4a513..170edbd 100644 --- a/flake.nix +++ b/flake.nix @@ -3,11 +3,7 @@ inputs = { logos-module-builder.url = "github:logos-co/logos-module-builder"; - - # The LEZ indexer Rust FFI lib + header come from this flake's `indexer` - # package output (/lib/libindexer_ffi.* + /include/indexer_ffi.h). It is a - # prebuilt Nix derivation, so mkExternalLib consumes it directly (no build). - logos-execution-zone.url = "git+https://github.com/logos-blockchain/logos-execution-zone?ref=refs/tags/v0.2.0"; + logos-execution-zone.url = "git+https://github.com/logos-blockchain/logos-execution-zone?ref=main"; }; outputs = diff --git a/metadata.json b/metadata.json index 3a54678..d70cf2d 100644 --- a/metadata.json +++ b/metadata.json @@ -1,5 +1,6 @@ { "name": "lez_indexer_module", + "display_name": "LEZ Indexer", "version": "1.0.0", "type": "core", "interface": "universal", diff --git a/src/lez_ffi_marshalling.cpp b/src/lez_ffi_marshalling.cpp index 6f8f3b2..9d63143 100644 --- a/src/lez_ffi_marshalling.cpp +++ b/src/lez_ffi_marshalling.cpp @@ -152,6 +152,19 @@ namespace marshalling { return true; } + // True iff `s` is exactly `num_chars` hex digits (no prefix/whitespace). + bool isHex(const std::string& s, const size_t num_chars) { + if (s.size() != num_chars) { + return false; + } + for (const char c : s) { + if (hexNibble(c) < 0) { + return false; + } + } + return true; + } + // Base58-encode `length` raw bytes (plain Bitcoin alphabet). Big-integer // base 256 -> 58, leading zero bytes map to leading '1's. Mirrors // Base58.js::encode so account ids match the wallet UI and canonical LEZ. diff --git a/src/lez_ffi_marshalling.h b/src/lez_ffi_marshalling.h index 0eae49b..86b102b 100644 --- a/src/lez_ffi_marshalling.h +++ b/src/lez_ffi_marshalling.h @@ -40,6 +40,11 @@ namespace marshalling { // Returns false unless it decodes to exactly 32 bytes. bool hexToBytes32(const std::string& hex, FfiBytes32* out); + // True iff `s` is exactly `num_chars` hex digits (no 0x prefix, no whitespace). + // For validating fixed-width hex identifiers (e.g. a 64-char channel id) before + // trusting them as-is, such as when composing a filesystem path. + bool isHex(const std::string& s, size_t num_chars); + // Base58 (plain Bitcoin alphabet, no checksum) of `length` raw bytes. This is // the canonical string form of an LEZ *account id* — it matches lee::AccountId // Display, wallet_ffi's account_id_to_base58, and the wallet UI's Base58.js. diff --git a/src/lez_indexer_module_impl.cpp b/src/lez_indexer_module_impl.cpp index f600b92..1314837 100644 --- a/src/lez_indexer_module_impl.cpp +++ b/src/lez_indexer_module_impl.cpp @@ -5,6 +5,9 @@ #include #include +#include +#include +#include using namespace marshalling; @@ -15,34 +18,62 @@ namespace { return static_cast(p); } - void warn(const char* method, const char* msg) { - std::fprintf(stderr, "lez_indexer_module: %s: %s\n", method, msg); + // Module diagnostics go to stderr; logos_host captures it and routes each line + // through its own logger, classifying by an "Error:"/"Warning:" token found + // anywhere in the line (plain lines are treated as info). Prefix accordingly. + void info(const char* method, const std::string& msg) { + std::fprintf(stderr, "lez_indexer_module: %s: %s\n", method, msg.c_str()); + } + void warn(const char* method, const std::string& msg) { + std::fprintf(stderr, "lez_indexer_module: Warning: %s: %s\n", method, msg.c_str()); + } + void error(const char* method, const std::string& msg) { + std::fprintf(stderr, "lez_indexer_module: Error: %s: %s\n", method, msg.c_str()); } } // namespace LezIndexerModuleImpl::~LezIndexerModuleImpl() { if (stop_indexer() != 0) { - warn("destructor", "indexer FFI error on stop"); + error("destructor", "indexer FFI error on stop"); } } // === Indexer Lifecycle === -int64_t LezIndexerModuleImpl::start_indexer(const std::string& config_path) { - if (!indexer_service_ffi) { - // Null runtime: the FFI creates and owns its own tokio runtime. Storage - // goes under this module's instance persistence path (host-owned, stable - // per Basecamp --user-dir) so RocksDB never lands in the process CWD. - InitializedIndexerServiceFFIResult res = - ::start_indexer(nullptr, config_path.c_str(), instancePersistencePath().c_str()); - if (is_error(&res.error)) { - warn("start_indexer", "indexer FFI error"); - return static_cast(res.error); - } +std::string LezIndexerModuleImpl::resolveStorageDir(const char* method) const { + // The host owns where state lives (its instance persistence path). When that + // isn't provisioned (e.g. running outside Basecamp), fall back to the process + // working directory — the same "." the FFI's start_indexer uses — so start and + // reset always agree on the store location. + const std::string& path = instancePersistencePath(); + if (!path.empty()) { + return path; + } + warn(method, "no instance persistence path; using the working directory"); + return "."; +} - indexer_service_ffi = res.value; +int64_t LezIndexerModuleImpl::start_indexer(const std::string& config_path) { + if (indexer_service_ffi) { + info("start_indexer", "indexer already running; ignoring start request"); + return 0; } + // Null runtime: the FFI creates and owns its own tokio runtime. + const std::string storage = resolveStorageDir("start_indexer"); + info("start_indexer", "starting indexer (config=" + config_path + ", storage=" + storage + ")"); + + InitializedIndexerServiceFFIResult res = ::start_indexer(nullptr, config_path.c_str(), storage.c_str()); + if (is_error(&res.error)) { + error( + "start_indexer", + "FFI failed to start indexer (OperationStatus " + std::to_string(static_cast(res.error)) + ")" + ); + return static_cast(res.error); + } + + indexer_service_ffi = res.value; + info("start_indexer", "indexer started"); return 0; } @@ -55,14 +86,60 @@ int64_t LezIndexerModuleImpl::stop_indexer() { OperationStatus operation_result = ::stop_indexer(handle(indexer_service_ffi)); indexer_service_ffi = nullptr; if (is_error(&operation_result)) { - warn("stop_indexer", "indexer FFI error on stop"); + error( + "stop_indexer", + "FFI error on stop (OperationStatus " + std::to_string(static_cast(operation_result)) + ")" + ); return static_cast(operation_result); } + info("stop_indexer", "indexer stopped"); return 0; } -void LezIndexerModuleImpl::init_logger(const std::string& level) { - ::init_logger(level.c_str()); +int64_t LezIndexerModuleImpl::reset_storage(const std::string& config_path) { + // Stop first so RocksDB is closed before its files are deleted + const int64_t stop_code = stop_indexer(); + if (stop_code != 0) { + error("reset_storage", "could not stop indexer before wiping storage"); + return stop_code; + } + + // FFI stores RocksDB at /rocksdb-{channel_id} + const std::filesystem::path storage = resolveStorageDir("reset_storage"); + std::string channel_id; + try { + std::ifstream in(config_path); + if (!in) { + error("reset_storage", "could not open config " + config_path); + return -1; + } + channel_id = nlohmann::json::parse(in).at("channel_id").get(); + } catch (const std::exception& e) { + error("reset_storage", std::string("could not read channel_id from config: ") + e.what()); + return -1; + } + // A channel id is a 32-byte hex string. Validate before building a path so a + // malformed/edited config can't inject separators ("../", absolute paths) and + // make remove_all escape the storage dir. + if (!isHex(channel_id, 64)) { + error("reset_storage", "config channel_id is not a 64-char hex string; refusing to build a wipe path"); + return -1; + } + + const std::filesystem::path store = storage / ("rocksdb-" + channel_id); + std::error_code ec; + // remove_all returns the number of entries removed and only sets `ec` on a real + // error; a missing store removes 0 with no error. No exists() pre-check — that + // would mask an IO/permission error as a successful "nothing to wipe". + const std::uintmax_t removed = std::filesystem::remove_all(store, ec); + if (ec) { + error("reset_storage", "failed to remove " + store.string() + ": " + ec.message()); + return -1; + } + info("reset_storage", + removed == 0 ? "no store at " + store.string() + "; nothing to wipe" + : "wiped rocksdb store " + store.string()); + return 0; } // === Indexer Queries === diff --git a/src/lez_indexer_module_impl.h b/src/lez_indexer_module_impl.h index 55d2340..b7e17d3 100644 --- a/src/lez_indexer_module_impl.h +++ b/src/lez_indexer_module_impl.h @@ -41,6 +41,16 @@ public: /// own). Returns 0 on success, else the FFI OperationStatus code. int64_t stop_indexer(); + /// Stop the indexer (if running) and delete the RocksDB store for the config's + /// channel — `/rocksdb-`, where `` is resolved the + /// same way start_indexer resolves it (the host's instance persistence path, or + /// the process working directory when unset) — so the next start_indexer + /// re-indexes from scratch. The recovery path when the store is stale against a + /// different/reset chain. Pass the same `config_path` given to start_indexer; + /// does NOT restart. Returns 0 on success, else non-zero. int64_t for the same + /// codegen reason as start_indexer. + int64_t reset_storage(const std::string& config_path); + /// Account by id, accepting Base58 (canonical) or 32-byte hex. The returned /// JSON omits the id; callers inject the queried id themselves. std::string getAccount(const std::string& account_id); @@ -71,12 +81,12 @@ public: std::string getTransactionsByAccount(const std::string& account_id, const std::string& offset, const std::string& limit); // clang-format on - /// Enable the indexer's logging at `level` (off/error/warn/info/debug/trace; - /// null or unparseable falls back to info). Scoped to the indexer crates - /// only; the first call wins (subsequent calls are no-ops). - void init_logger(const std::string& level); - private: + // Storage dir handed to the FFI, either: + // - the host's instance persistence path + // - the process working directory (".") when the host didn't provision one + std::string resolveStorageDir(const char* method) const; + // IndexerServiceFFI* — opaque here (see class comment); cast in the .cpp. void* indexer_service_ffi = nullptr; };