From 7eac1f8335a8f5689bd060cc9db03f41a8a3fc02 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Mon, 13 Apr 2026 11:52:39 -0500 Subject: [PATCH 01/32] trace_api: enforce startup continuity and detect snapshot overlap Add first_recorded_block() and last_recorded_block() to slice_directory and store_provider by scanning the highest/lowest index slice files. On the first block_start signal after startup, check_continuity() in chain_extraction_impl_type validates the relationship between existing trace data and the current chain head: - No prior data: fresh start, log and proceed. - Chain head within [first_recorded, last_recorded+1]: overlap or exact continuation; re-applied blocks overwrite existing slice entries, which allows recovery from disk corruption by replaying from a snapshot. - Chain head < first_recorded: snapshot predates trace history start, error with operator guidance to delete the trace directory. - Chain head > last_recorded+1: forward gap, error with guidance. 12 new unit tests cover all cases in test_continuity.cpp. --- docs/trace-api-history-plan.md | 307 ++++++++++++++++++ .../sysio/trace_api/chain_extraction.hpp | 38 +++ .../sysio/trace_api/store_provider.hpp | 22 ++ .../trace_api_plugin/src/store_provider.cpp | 71 ++++ .../trace_api_plugin/src/trace_api_plugin.cpp | 8 + plugins/trace_api_plugin/test/CMakeLists.txt | 1 + .../trace_api_plugin/test/test_continuity.cpp | 139 ++++++++ .../trace_api_plugin/test/test_extraction.cpp | 12 + 8 files changed, 598 insertions(+) create mode 100644 docs/trace-api-history-plan.md create mode 100644 plugins/trace_api_plugin/test/test_continuity.cpp diff --git a/docs/trace-api-history-plan.md b/docs/trace-api-history-plan.md new file mode 100644 index 0000000000..8b9764bb68 --- /dev/null +++ b/docs/trace-api-history-plan.md @@ -0,0 +1,307 @@ +# trace_api_plugin: History-Solution Upgrade Plan + +Single PR, multiple commits. Each commit is independently reviewable and (where possible) independently testable. + +## Goals + +1. Fix `/v1/trace_api/get_transaction_trace` full-history scan. +2. Capture ABIs automatically (no operator-supplied `abi.json`), versioned correctly across mid-block / mid-trx setabi. +3. Refuse to start with a gap in recorded blocks (correctness foundation for #2). +4. Add exchange-friendly query endpoints over the existing trace data. + +Non-goals: account-history index, cross-contract joins, streaming, public-internet hardening, ABI sidecar pruning. + +## Storage shape (new sidecars, alongside existing slice files) + +All new files live in the same slice dir as `trace_*.log`: + +| File | Scope | Purpose | +|---|---|---| +| `trace_trx_idx_.log` | per slice | mmap'd hash table: `trx_id_prefix64 -> block_num` | +| `abi_blobs.log` | global | ABI blobs, append-only | +| `abi_index.log` | global | append-only `(account, abi_global_seq, blob_offset, blob_len)` | + +ABI sidecar is global (not per-slice) because ABI versions cross slice boundaries. + +## Commits + +### Commit 1: Continuity enforcement at startup + +**Why:** Foundation. Without this, ABI lazy-fetch reasoning breaks (you can't claim "if a setabi happened between fresh-start and first-encounter we'd have seen it" if gaps are silently allowed). + +**Changes:** +- `slice_directory`: add `last_recorded_block()` — scan highest index slice file front-to-back, return max `block_entry_v0.number`, or `nullopt` if no slice files exist. +- `store_provider`: expose `last_recorded_block()` delegating to `_slice_directory`. +- `chain_extraction_impl`: on first `signal_block_start` after startup, check: + - empty dir (`nullopt`): log fresh-start at head, proceed. + - non-empty dir: require `block_num == last_recorded_block + 1`, else invoke `except_handler` with a descriptive message ("delete slice dir or replay from snapshot covering blocks N..M"), which causes node shutdown. + +**Tests:** unit tests for all continuity cases: fresh start, exact continuation, overlap (snapshot within existing data — ok), snapshot before data start (error), gap forward (error), check fires only once. + +--- + +### Commit 2: trx_id index sidecar + +**Why:** Replace O(N slices * slice scan) with O(slices) page-faults. + +**File format:** `trace_trx_idx_.log` +``` +header { magic, version, bucket_count, bucket_size } +buckets[bucket_count] of { trx_id_prefix64, block_num, _pad } // open addressing +``` +Open-addressing hash, load factor ~0.5, linear probing. Sized at slice close from observed trx count. No bloom filter in v1 — hash probe is already O(1) per slice; revisit if not-found latency becomes a real complaint. Header has room to add a bloom section later without breaking old readers. + +**Changes:** +- `trx_id_index_writer`: builds index from a closed `trace_trx_id_.log`. Iterates entries, fills buckets, writes file. +- `trx_id_index_reader`: mmap's file, `lookup(trx_id) -> optional`. +- `slice_directory`: `find_trx_id_index_slice(slice_num)`, parallels `find_trx_id_slice`. +- Maintenance thread: when a slice's trx_id file becomes stable (LIB-crossed or compressed), build the index. Idempotent (skip if exists, rebuild if header version mismatches). +- Startup: scan slice dir, build any missing indexes synchronously before serving requests (optional fast path: do it lazily on first miss with a one-time background build). +- Rewrite `store_provider::get_trx_block_number`: + - iterate slices newest -> oldest + - per slice: bucket probe + - on hash hit, confirm by reading the actual `block_trxs_entry` to defeat 64-bit prefix collisions + - fall back to current linear scan only if a slice's index file is missing/corrupt (log warning, schedule rebuild) + +**Tests:** round-trip insert/lookup; collision handling (synthetic colliding ids); missing-index rebuild on startup; pruning when slice deleted (index file deleted alongside). + +--- + +### Commit 3: ABI sidecar — storage layer + +**Why:** Foundation for commits 4 and 5; pure data-layer with no chain hooks yet. + +**Changes:** +- `abi_blob_store`: append-only `abi_blobs.log`. `store(bytes) -> {offset, len}`. No dedup — ABIs are small (few KB) and duplication across a node's lifetime is negligible. +- `abi_version_index`: in-memory `map>`, persisted as append-only records to `abi_index.log`. Startup does a single pass over the file to populate the in-memory map. +- `lookup(account, action_global_seq) -> optional`: `upper_bound` on per-account vector, step back one. `0` sentinel matches anything. +- `record(account, abi_global_seq, abi_bytes)`: append blob, append index entry, update in-memory map. + +**Tests:** version lookup with `<=` semantics including `0` sentinel, restart replay produces identical in-memory map. + +--- + +### Commit 4: ABI capture — live + lazy + +**Why:** Wires sidecar to chain. + +**Changes:** +- In `chain_extraction_impl::on_applied_transaction`, walk action_traces: + - if `act.account == "sysio" && act.name == "setabi"`: decode payload `{account, abi: bytes}`, call `abi_version_index.record(account, act.receipt->global_sequence, abi)`. Invalidate decoder cache for that account. + - for any other action: ensure ABI exists for `act.receiver` (the contract whose code ran). If `abi_version_index.lookup(receiver, 0)` is empty, fetch current ABI from `controller.db().find(receiver)` and `record(receiver, 0, current_abi)`. (Sentinel 0 = "as of beginning of observation, exact block unknown.") +- Decoder helper `decode_action_data(account, action_global_seq, action_name, data) -> variant`: + - `lookup` ABI bytes for that version + - construct `abi_serializer` (cache by `(account, abi_global_seq)`) + - on any throw: return raw bytes + flag + +**Tests:** mid-trx setabi changes ABI for sibling inline action; lazy capture on first encounter; cache invalidation on setabi; corrupt ABI on chain falls back to raw. + +--- + +### Commit 5: `get_actions` primitive endpoint + +**Endpoint:** `POST /v1/trace_api/get_actions` + +**Request:** +```json +{ + "contract": "sysio.token", // required + "action": "transfer", // optional + "block_num": 12345, // OR + "block_range": [12345, 12400], // capped by trace-max-block-range + "filters": { + "from": "alice", // optional + "to": "bob", // optional + "authorizer": "alice" // optional + }, + "include_failed": false // default false; when false, only "executed" +} +``` + +**Behavior:** +- Always traverses inline actions (no toggle). +- Match rule: `act.account == contract` (canonical action, not notification). Notifications surface as separate rows when `act.receiver == contract` but `act.account != contract` — excluded by default; reconsider only if needed. +- Range scan: iterate `block_num` in range, load block trace from existing slice infra, walk action_traces. +- Filters applied post-load. +- ABI decode via commit-4 helper; raw fallback on error. + +**Response row:** +```json +{ + "block_num": 12345, + "trx_id": "...", + "action_ordinal": 3, + "global_sequence": 9876543, + "receiver": "sysio.token", + "account": "sysio.token", + "name": "transfer", + "authorization": [...], + "status": "executed", + "irreversible": true, + "data_decoded": {...}, // present if decode succeeded + "data_raw": "..." // present only if decode failed +} +``` + +**Config:** +- `trace-max-block-range` (default e.g. 1000; -1 = unlimited for local-only deployments). + +**Tests:** filters; failed-trx exclusion/inclusion; inline traversal; range cap; decode-failure raw fallback. + +--- + +### Commit 6: `get_token_transfers` convenience endpoint + +**Endpoint:** `POST /v1/trace_api/get_token_transfers` + +**Request:** same shape as `get_actions` minus `action`, plus: +```json +{ + "contract": "sysio.token", // required, exact + "account": "alice", // optional, matches from OR to + "memo_contains": "user_42", // optional substring + "block_num" | "block_range": ..., + "include_failed": false +} +``` + +**Behavior:** thin wrapper over `get_actions` with `action="transfer"`, projects decoded data: +```json +{ + "block_num": ..., + "trx_id": ..., + "action_ordinal": ..., + "global_sequence": ..., + "from": "...", + "to": "...", + "quantity": "1.0000 SYS", + "memo": "...", + "status": "executed", + "irreversible": true +} +``` + +If decode fails (ABI invalid), the row is **omitted** (not raw — exchanges can't use raw transfers; they'd see them via `get_actions` if needed). Log a warning with trx_id for diagnosis. + +**Tests:** from/to OR semantics; memo substring; multiple transfers in one block; decode-failure omission. + +--- + +## Risks / open items + +- **Reorg of indexed data.** Trx_id index covers irreversible blocks only. With fast finality the reversible window is tiny (seconds, low single-digit blocks), and most operators are expected to run trace_api in read-mode=irreversible. Lookup path: index probe first; on miss, linear-scan the reversible window (cheap because the window is small) before returning not-found. No reorg fixup needed. +- **`abi_index.log` corruption.** Append-only with no checksums today. Acceptable v1; add CRC per record in a follow-up if it bites. +- **Continuity check + existing deployments.** Operators with existing slice dirs and gaps will fail to start after upgrade. Release note + `trace-allow-gap=true` escape hatch. + +## Test plan + +The trace_api_plugin has unusually strong test coverage today (see `plugins/trace_api_plugin/test/`: `test_trace_file`, `test_data_handlers`, `test_extraction`, `test_responses`, `test_compressed_file`, `test_configuration_utils`). Maintain that bar — every new component gets a dedicated test file with the same breadth. + +### Commit 1 — continuity enforcement + +New test file: `test/test_continuity.cpp`. + +- Empty slice dir, chain head at block 1 -> starts, logs fresh-start. +- Empty slice dir, chain head at block 50M -> starts, logs fresh-start at 50M. +- Non-empty slice dir ending at block N, chain head at N+1 -> starts normally. +- Non-empty slice dir ending at block N, chain head at N+10 -> throws with operator-facing message naming the gap. +- Non-empty slice dir ending at block N, chain head at N-5 (replay from snapshot behind tip) -> starts normally once chain catches up; the first accepted_block after startup is N+1. +- `trace-allow-gap=true` config override -> legacy behavior, warns loudly. +- Slice dir contains only orphaned index file (no trace/trx_id) -> treat as empty (or throw — pick and test). +- Corrupted highest slice file -> clear error, does not proceed. + +### Commit 2 — trx_id index + +New test file: `test/test_trx_id_index.cpp`. + +- Writer: build from synthetic `trace_trx_id_.log`, verify bucket occupancy, linear probe chains terminate, load factor within spec. +- Reader: lookup hit, lookup miss, lookup with prefix collision (two trx_ids sharing the 64-bit prefix) -> second probe, correctness verified against underlying trx_id entry. +- Round-trip: random 10k trx_ids, every one found; 10k never-inserted trx_ids, all return not-found. +- File format: wrong magic -> error; wrong version -> error; truncated file -> error (not UB). +- mmap lifecycle: reader holds file, writer cannot overwrite (or does via new file + rename). +- Startup missing-index rebuild: delete one slice's index, start, verify it's rebuilt, lookup still works. +- Corrupt index file on startup: rebuilt from the trx_id log source-of-truth. + +Extensions to existing tests: + +- `test_extraction.cpp`: verify index is built when slice closes (on LIB cross). +- Integration test under `tests/`: run against a 100k-block replay dataset, measure and assert `get_transaction_trace` latency under a fixed budget (regression guard). +- Reversible window: lookup for a trx in the current reversible window hits via the fallback linear scan. + +### Commit 3 — ABI sidecar storage + +New test file: `test/test_abi_sidecar.cpp`. + +- `abi_blob_store` append + read round-trip across many records. +- `abi_version_index`: + - `lookup(acct, global_seq)` with `<=` semantics: boundary at exact match, just-before, just-after. + - `0` sentinel entry matches any `global_seq`. + - Multiple versions same account, different global_seqs -> returns correct one. + - Lookup for unknown account -> empty. + - Lookup with `global_seq` before the earliest recorded -> empty (not the `0` entry, unless a `0` entry exists). +- Restart replay: write N records across process lifetime, restart, in-memory map identical to pre-restart (compare via dump-to-vector). +- Corrupt `abi_index.log` tail: recover up to last valid record, log; do not crash. +- Mid-block multiple setabi for same account: both stored, lookup between them returns the earlier. + +### Commit 4 — ABI capture + +Extensions to `test_extraction.cpp` (add new cases; do not fork the file unless it grows unwieldy): + +- Live capture: `setabi` action in applied_transaction -> sidecar has new record with correct `global_sequence`. +- Lazy capture: first action from never-before-seen account -> `lookup(account, 0)` populated from controller state. +- Cache invalidation: action -> lazy fetch; then setabi observed; next decode for that account uses the new ABI. +- Mid-trx setabi: trx has action A (account X), then setabi for X, then action B (account X). A decodes with old ABI, B decodes with new ABI. Verified via action_global_seq boundaries. +- Inline setabi: same as above but setabi is inline, not top-level. +- Plugin started at block 50M on a chain where X did setabi at block 40M and never again: first encounter lazy-fetches current ABI tagged at 0 — decode succeeds. +- Invalid ABI on chain (bytes that don't deserialize into an `abi_def`): sidecar records raw bytes; decoder returns raw on decode attempt; does not throw through to endpoint. +- Decoder cache hit counting (optional, via hook): verify cache actually reused across same `(account, abi_global_seq)`. + +### Commit 5 — get_actions endpoint + +Extensions to `test_responses.cpp`: + +- Single block, single contract, single matching action -> returns one row with decoded data. +- Single block, no matching actions -> empty response, 200. +- Multiple matching actions across inline tree -> all returned in action_ordinal order. +- Filters combined: `contract + action`, `+from`, `+to`, `+authorizer`, mutually constraining. +- `include_failed=false` (default): `soft_fail`, `hard_fail`, `expired`, `delayed` excluded. +- `include_failed=true`: all statuses returned with correct `status` field. +- Block range: full range scanned, results in order. +- Range cap: request exceeding `trace-max-block-range` -> 400 with the cap value. +- `max_block_range=-1` (unlimited): large range works. +- ABI present, decode succeeds: `data_decoded` present, no `data_raw`. +- ABI present, decode fails (invalid bytes for the schema): `data_raw` present, `data_decoded` absent, with `decode_error`. +- Reversible block in range: included with `irreversible: false`. +- Contract the endpoint has never seen an ABI for: lazy fetch kicks in at query time or ingestion time (confirm which; test accordingly). +- Notifications vs canonical actions: only `act.account == contract` rows returned (notifications excluded). + +### Commit 6 — get_token_transfers + +New test file: `test/test_token_transfers.cpp`. + +- Simple transfer: `from/to/quantity/memo` projected correctly. +- `account` filter = from OR to: matches either side. +- `memo_contains`: substring match, case-sensitive; non-match excluded; empty memo never matches a non-empty pattern. +- Multiple transfers in one block: all returned, ordered by `(block_num, action_ordinal)`. +- Inline-action transfer: returned. +- Failed transfer (hard_fail): excluded by default, included with `include_failed=true`. +- ABI decode failure: row omitted, warning logged, endpoint succeeds. +- Non-token contract with a `transfer` action of different schema: decode fails -> row omitted. (Caller was told to scope correctly; we just don't return garbage.) +- `contract` required -> 400 when omitted. +- Range semantics match `get_actions` (reuse shared test helpers). + +### Cross-cutting integration tests (under `tests/`) + +- End-to-end: launch a TestHarness cluster with trace_api enabled, deploy sysio.token, perform 1000 transfers with a mix of inline/top-level, setabi mid-run on the token contract changing the memo type, then query: + - `get_transaction_trace` for each trx_id — all hit, latency asserted. + - `get_token_transfers` for the token, each block in range, all transfers accounted for, old transfers decoded with old ABI, new ones with new ABI. +- Gap-refusal: kill the node, advance chain via a second node, restart first node with trace_api -> refuse to start. +- Restart idempotency: start, run 1000 blocks, stop, restart — sidecars/indexes intact, queries unchanged. +- Compressed slice interaction: force a slice compression, verify trx_id lookup still works against the `.clog` slice (via the existing compressed-slice read path). +- Retention pruning: set `minimum_irreversible_history_blocks` low, verify slice deletion also removes the matching `trace_trx_idx_*` file; verify ABI sidecar is untouched. + +### Performance regression guard + +Add a micro-benchmark (gated, not run in default ctest): + +- 100k-block synthetic trace corpus, 10k random trx_id lookups: p50 and p99 latency must be within a configured budget (e.g., p99 < 5 ms on SSD). Fails the guard if we regress to linear-scan behavior. diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp index be25f22136..7fff78c658 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -65,9 +66,45 @@ class chain_extraction_impl_type { } void on_block_start( uint32_t block_num ) { + if (!_startup_checked) { + _startup_checked = true; + check_continuity(block_num); + } clear_caches(); } + void check_continuity(uint32_t block_num) { + try { + const auto first = store.first_recorded_block(); + const auto last = store.last_recorded_block(); + if (!first) { + ilog("trace_api: no prior trace data found, starting fresh at block {}", block_num); + return; + } + // Overlap or exact continuation: chain head is within or just past existing data. + // Re-applied blocks will overwrite existing slice entries as they are re-recorded. + if (block_num >= *first && block_num <= *last + 1) + return; + + if (block_num < *first) { + throw std::runtime_error( + std::string("trace_api: chain head (") + std::to_string(block_num) + + ") is before the first recorded trace block (" + std::to_string(*first) + + "). Delete the trace directory and restart to record from the snapshot point."); + } + // block_num > last + 1: forward gap + throw std::runtime_error( + std::string("trace_api: gap detected in trace data. Last recorded block: ") + + std::to_string(*last) + ", current block: " + std::to_string(block_num) + + ". Delete the trace directory and restart to begin fresh, or load a snapshot " + "that continues from or before block " + std::to_string(*last + 1) + "."); + } catch (const yield_exception&) { + throw; + } catch (...) { + except_handler(MAKE_EXCEPTION_WITH_CONTEXT(std::current_exception())); + } + } + void clear_caches() { cached_traces.clear(); onblock_trace.reset(); @@ -118,6 +155,7 @@ class chain_extraction_impl_type { exception_handler except_handler; std::map cached_traces; std::optional onblock_trace; + bool _startup_checked{false}; }; diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp index 6de1a874c9..8208f8ac7d 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp @@ -209,6 +209,17 @@ namespace sysio::trace_api { */ void for_each_trx_id_slice(std::function callback) const; + /** + * Return the lowest block number recorded in any index slice file, or nullopt if no data exists. + */ + std::optional first_recorded_block() const; + + /** + * Return the highest block number recorded in any index slice file, or nullopt if no data exists. + * Used at startup to detect gaps between existing trace data and the current chain head. + */ + std::optional last_recorded_block() const; + /** * set the LIB for maintenance * @param lib @@ -288,6 +299,17 @@ namespace sysio::trace_api { get_block_n get_trx_block_number(const chain::transaction_id_type& trx_id, const yield_function& yield= {}); + /** + * Return the lowest block number recorded in any index slice file, or nullopt if no data exists. + */ + std::optional first_recorded_block() const; + + /** + * Return the highest block number recorded in any index slice file, or nullopt if the slice directory + * is empty. Used at startup to verify continuity between existing trace data and the current chain head. + */ + std::optional last_recorded_block() const; + void start_maintenance_thread( log_handler log ) { _slice_directory.start_maintenance_thread( std::move(log) ); } diff --git a/plugins/trace_api_plugin/src/store_provider.cpp b/plugins/trace_api_plugin/src/store_provider.cpp index 4c7bbccc1c..19a9b43217 100644 --- a/plugins/trace_api_plugin/src/store_provider.cpp +++ b/plugins/trace_api_plugin/src/store_provider.cpp @@ -312,6 +312,77 @@ namespace sysio::trace_api { } } + namespace { + // Collect and sort all trace_index_*.log paths; ascending=true gives lowest slice first. + std::vector collect_index_paths(const std::filesystem::path& slice_dir, bool ascending) { + namespace fs = std::filesystem; + std::vector paths; + for (const auto& entry : fs::directory_iterator(slice_dir)) { + if (!entry.is_regular_file()) continue; + const auto name = entry.path().filename().string(); + if (name.rfind(_trace_index_prefix, 0) == 0 && entry.path().extension() == _trace_ext) + paths.push_back(entry.path()); + } + if (ascending) + std::sort(paths.begin(), paths.end()); + else + std::sort(paths.begin(), paths.end(), std::greater<>()); + return paths; + } + + // Scan a single index slice file and return the min and max block numbers found. + std::optional> scan_index_slice(const std::filesystem::path& path, uint32_t current_version) { + try { + fc::cfile index; + index.set_file_path(path); + index.open("rb"); + index.seek(0); + const auto header = extract_store(index); + if (header.version != current_version) + return std::nullopt; + const uint64_t end = file_size(path); + std::optional lo, hi; + while (index.tellp() < end) { + const auto e = extract_store(index); + if (std::holds_alternative(e)) { + const auto num = std::get(e).number; + if (!lo || num < *lo) lo = num; + if (!hi || num > *hi) hi = num; + } + } + if (lo && hi) + return std::make_pair(*lo, *hi); + } catch (...) { + // malformed or partially written slice + } + return std::nullopt; + } + } // anonymous namespace + + std::optional slice_directory::first_recorded_block() const { + for (const auto& path : collect_index_paths(_slice_dir, /*ascending=*/true)) { + if (const auto r = scan_index_slice(path, _current_version)) + return r->first; + } + return std::nullopt; + } + + std::optional slice_directory::last_recorded_block() const { + for (const auto& path : collect_index_paths(_slice_dir, /*ascending=*/false)) { + if (const auto r = scan_index_slice(path, _current_version)) + return r->second; + } + return std::nullopt; + } + + std::optional store_provider::first_recorded_block() const { + return _slice_directory.first_recorded_block(); + } + + std::optional store_provider::last_recorded_block() const { + return _slice_directory.last_recorded_block(); + } + void slice_directory::set_lib(uint32_t lib) { { std::scoped_lock lock(_maintenance_mtx); diff --git a/plugins/trace_api_plugin/src/trace_api_plugin.cpp b/plugins/trace_api_plugin/src/trace_api_plugin.cpp index 1457919659..74f13319ab 100644 --- a/plugins/trace_api_plugin/src/trace_api_plugin.cpp +++ b/plugins/trace_api_plugin/src/trace_api_plugin.cpp @@ -91,6 +91,14 @@ namespace { store->append_trx_ids(std::move(tt)); } + std::optional first_recorded_block() const { + return store->first_recorded_block(); + } + + std::optional last_recorded_block() const { + return store->last_recorded_block(); + } + std::shared_ptr store; }; } diff --git a/plugins/trace_api_plugin/test/CMakeLists.txt b/plugins/trace_api_plugin/test/CMakeLists.txt index aab0919ff5..6b75f0f303 100644 --- a/plugins/trace_api_plugin/test/CMakeLists.txt +++ b/plugins/trace_api_plugin/test/CMakeLists.txt @@ -1,4 +1,5 @@ add_executable( test_trace_api_plugin + test_continuity.cpp test_extraction.cpp test_responses.cpp test_trace_file.cpp diff --git a/plugins/trace_api_plugin/test/test_continuity.cpp b/plugins/trace_api_plugin/test/test_continuity.cpp new file mode 100644 index 0000000000..d639ca5cbf --- /dev/null +++ b/plugins/trace_api_plugin/test/test_continuity.cpp @@ -0,0 +1,139 @@ +#include + +#include +#include + +using namespace sysio; +using namespace sysio::trace_api; +using namespace sysio::trace_api::test_common; + +namespace { + +struct continuity_mock_store { + // first == nullopt means no data at all + continuity_mock_store(std::optional first_block, std::optional last_block) + : _first_block(first_block), _last_block(last_block) {} + + template + void append(const BlockTrace&) {} + void append_lib(uint32_t) {} + void append_trx_ids(block_trxs_entry) {} + + std::optional first_recorded_block() const { return _first_block; } + std::optional last_recorded_block() const { return _last_block; } + + std::optional _first_block; + std::optional _last_block; +}; + +struct continuity_fixture { + // Convenience: data [first, last] exists, or nullopt for empty store + continuity_fixture(std::optional first_block, std::optional last_block) + : store_first(first_block), store_last(last_block) + {} + + bool try_block_start(uint32_t block_num) { + bool threw = false; + auto except = exception_handler{[&threw](const exception_with_context&) { + threw = true; + throw yield_exception("continuity error"); + }}; + chain_extraction_impl_type impl( + continuity_mock_store{store_first, store_last}, std::move(except)); + try { + impl.signal_block_start(block_num); + } catch (const yield_exception&) { + // expected on continuity error + } + return !threw; + } + + std::optional store_first; + std::optional store_last; +}; + +} // namespace + +BOOST_AUTO_TEST_SUITE(continuity_tests) + + // Empty slice dir: any starting block is a fresh start, always succeeds + BOOST_AUTO_TEST_CASE(fresh_start_block_1) { + continuity_fixture f(std::nullopt, std::nullopt); + BOOST_CHECK(f.try_block_start(1)); + } + + BOOST_AUTO_TEST_CASE(fresh_start_high_block) { + continuity_fixture f(std::nullopt, std::nullopt); + BOOST_CHECK(f.try_block_start(50'000'000)); + } + + // Exact continuation: chain head == last_recorded + 1 + BOOST_AUTO_TEST_CASE(exact_continuation_from_block_1) { + continuity_fixture f(1, 1); + BOOST_CHECK(f.try_block_start(2)); + } + + BOOST_AUTO_TEST_CASE(exact_continuation_mid_chain) { + continuity_fixture f(1, 50'000'000); + BOOST_CHECK(f.try_block_start(50'000'001)); + } + + // Overlap: chain head is within existing data range (snapshot older than trace end) + // Should succeed — re-applied blocks will overwrite. + BOOST_AUTO_TEST_CASE(overlap_at_first_recorded) { + continuity_fixture f(500, 600); + BOOST_CHECK(f.try_block_start(500)); // chain head == first recorded + } + + BOOST_AUTO_TEST_CASE(overlap_mid_range) { + continuity_fixture f(500, 600); + BOOST_CHECK(f.try_block_start(550)); // chain head in middle of existing data + } + + BOOST_AUTO_TEST_CASE(overlap_at_last_recorded) { + continuity_fixture f(500, 600); + BOOST_CHECK(f.try_block_start(600)); // chain head == last recorded (last block replay) + } + + // Gap forward: chain head > last_recorded + 1, must error + BOOST_AUTO_TEST_CASE(gap_forward_small) { + continuity_fixture f(1, 100); + BOOST_CHECK(!f.try_block_start(105)); + } + + BOOST_AUTO_TEST_CASE(gap_forward_large) { + continuity_fixture f(1, 50'000'000); + BOOST_CHECK(!f.try_block_start(60'000'000)); + } + + // Snapshot before trace data begins: chain head < first_recorded, must error + BOOST_AUTO_TEST_CASE(snapshot_before_data_start) { + continuity_fixture f(500, 600); + BOOST_CHECK(!f.try_block_start(400)); // chain head < first recorded + } + + BOOST_AUTO_TEST_CASE(snapshot_well_before_data_start) { + continuity_fixture f(50'000'000, 60'000'000); + BOOST_CHECK(!f.try_block_start(1)); + } + + // check_continuity called only once: subsequent block_start calls do not re-check + BOOST_AUTO_TEST_CASE(check_only_on_first_block_start) { + bool threw = false; + auto except = exception_handler{[&threw](const exception_with_context&) { + threw = true; + throw yield_exception("continuity error"); + }}; + + // fresh start, no prior data + chain_extraction_impl_type impl( + continuity_mock_store{std::nullopt, std::nullopt}, std::move(except)); + + impl.signal_block_start(100); // first call: fresh start, succeeds + BOOST_CHECK(!threw); + + impl.signal_block_start(200); // second call: no re-check, also succeeds + BOOST_CHECK(!threw); + } + +BOOST_AUTO_TEST_SUITE_END() diff --git a/plugins/trace_api_plugin/test/test_extraction.cpp b/plugins/trace_api_plugin/test/test_extraction.cpp index e0610dec76..f1bf1a0ce8 100644 --- a/plugins/trace_api_plugin/test/test_extraction.cpp +++ b/plugins/trace_api_plugin/test/test_extraction.cpp @@ -105,6 +105,14 @@ struct extraction_test_fixture { fixture.id_log[tt.block_num] = tt.ids; } + std::optional first_recorded_block() const { + return std::nullopt; // no prior data in unit tests + } + + std::optional last_recorded_block() const { + return std::nullopt; // no prior data in unit tests + } + extraction_test_fixture& fixture; }; @@ -113,6 +121,10 @@ struct extraction_test_fixture { { } + void signal_block_start( uint32_t block_num ) { + extraction_impl.signal_block_start(block_num); + } + void signal_applied_transaction( const chain::transaction_trace_ptr& trace, const chain::packed_transaction_ptr& ptrx ) { extraction_impl.signal_applied_transaction(trace, ptrx); } From e769d8fd17ebd4144115c0b01235e12adc0b2bbc Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Mon, 13 Apr 2026 12:27:23 -0500 Subject: [PATCH 02/32] trace_api: add per-slice trx_id hash index for O(1) lookups Replaces the O(n) linear scan in get_trx_block_number() with a compact open-addressing hash table (load factor <=0.5, power-of-2 bucket count, linear probing) written as a trace_trx_idx_.log sidecar per slice. The maintenance thread builds the index atomically (write .tmp, rename) after each slice becomes irreversible. Lookups do a per-slice fast-path through the reader; the linear scan fallback remains for slices not yet indexed. Also fixes _max_filename_size to account for the 14-char "trace_trx_idx_" prefix (was sized for the 12-char "trace_index_" prefix). --- .../sysio/trace_api/store_provider.hpp | 19 ++ .../include/sysio/trace_api/trx_id_index.hpp | 87 +++++++ .../trace_api_plugin/src/store_provider.cpp | 99 +++++++- plugins/trace_api_plugin/src/trx_id_index.cpp | 115 +++++++++ plugins/trace_api_plugin/test/CMakeLists.txt | 1 + .../test/test_trx_id_index.cpp | 232 ++++++++++++++++++ 6 files changed, 545 insertions(+), 8 deletions(-) create mode 100644 plugins/trace_api_plugin/include/sysio/trace_api/trx_id_index.hpp create mode 100644 plugins/trace_api_plugin/src/trx_id_index.cpp create mode 100644 plugins/trace_api_plugin/test/test_trx_id_index.cpp diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp index 8208f8ac7d..a47ce1690b 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace sysio::trace_api { @@ -209,6 +210,23 @@ namespace sysio::trace_api { */ void for_each_trx_id_slice(std::function callback) const; + /** + * Derive the slice number from a trx_id slice file path. + * Parses the block-range start from the filename. + */ + uint32_t slice_number_from_path(const std::filesystem::path& trx_id_path) const; + + /** + * Find the trx_id index file for a given slice number (or return nullopt if not present). + */ + std::optional find_trx_id_index_slice(uint32_t slice_number) const; + + /** + * Build the trx_id index for a given slice from its trx_id log file. + * No-op if the index already exists for that slice. + */ + void build_trx_id_index(uint32_t slice_number, const log_handler& log); + /** * Return the lowest block number recorded in any index slice file, or nullopt if no data exists. */ @@ -265,6 +283,7 @@ namespace sysio::trace_api { std::optional _last_cleaned_up_slice; const std::optional _minimum_uncompressed_irreversible_history_blocks; std::optional _last_compressed_slice; + std::optional _last_indexed_slice; const size_t _compression_seek_point_stride; std::mutex _maintenance_mtx; diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/trx_id_index.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/trx_id_index.hpp new file mode 100644 index 0000000000..5850c941ad --- /dev/null +++ b/plugins/trace_api_plugin/include/sysio/trace_api/trx_id_index.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace sysio::trace_api { + +// --------------------------------------------------------------------------- +// On-disk format: trace_trx_idx_.log +// +// Layout: trx_id_index_header (16 bytes) followed immediately by +// bucket_count trx_id_bucket records (16 bytes each). +// +// Serialized with fc::raw (native-endian, same convention as all other +// trace slice files — x86_64 Linux only). +// +// Open-addressing hash table with linear probing, load factor <= 0.5. +// bucket_count is always a power of two (modulo via bitwise AND). +// Empty slot sentinel: block_num == 0. SYSIO block numbers start at 1. +// --------------------------------------------------------------------------- + +struct trx_id_index_header { + static constexpr uint32_t magic_value = 0x54524958; // "TRIX" + static constexpr uint32_t current_version = 1; + + uint32_t magic = magic_value; + uint32_t version = current_version; + uint32_t bucket_count = 0; + uint32_t _reserved = 0; +}; +static_assert(sizeof(trx_id_index_header) == 16); + +struct trx_id_bucket { + uint64_t prefix64 = 0; // first 8 bytes of trx sha256 interpreted as uint64_t + uint32_t block_num = 0; // 0 = empty; SYSIO block numbers start at 1 + uint32_t _reserved = 0; +}; +static_assert(sizeof(trx_id_bucket) == 16); + +// --------------------------------------------------------------------------- +// Writer: accumulate (trx_id, block_num) pairs, write index file when done. +// --------------------------------------------------------------------------- + +class trx_id_index_writer { +public: + void add(const chain::transaction_id_type& trx_id, uint32_t block_num); + void write(const std::filesystem::path& path) const; + size_t entry_count() const { return _entries.size(); } + +private: + static uint64_t prefix_of(const chain::transaction_id_type& id); + + // last write wins per prefix (latest fork block overwrites earlier entries) + std::vector> _entries; +}; + +// --------------------------------------------------------------------------- +// Reader: load the index from disk and answer prefix lookups. +// Collisions (two trx_ids sharing a 64-bit prefix) are not explicitly +// confirmed — collisions in irreversible sha256 data are negligible. +// --------------------------------------------------------------------------- + +class trx_id_index_reader { +public: + explicit trx_id_index_reader(const std::filesystem::path& path); + + bool valid() const { return _valid; } + + // Returns the block_num for trx_id's prefix, or nullopt if not found. + std::optional lookup(const chain::transaction_id_type& trx_id) const; + +private: + static uint64_t prefix_of(const chain::transaction_id_type& id); + + std::vector _buckets; + bool _valid{false}; +}; + +} // namespace sysio::trace_api + +FC_REFLECT(sysio::trace_api::trx_id_index_header, (magic)(version)(bucket_count)(_reserved)) +FC_REFLECT(sysio::trace_api::trx_id_bucket, (prefix64)(block_num)(_reserved)) diff --git a/plugins/trace_api_plugin/src/store_provider.cpp b/plugins/trace_api_plugin/src/store_provider.cpp index 19a9b43217..0b01477eda 100644 --- a/plugins/trace_api_plugin/src/store_provider.cpp +++ b/plugins/trace_api_plugin/src/store_provider.cpp @@ -1,5 +1,6 @@ #include +#include #include #include @@ -8,9 +9,11 @@ namespace { static constexpr const char* _trace_prefix = "trace_"; static constexpr const char* _trace_index_prefix = "trace_index_"; static constexpr const char* _trace_trx_id_prefix = "trace_trx_id_"; + static constexpr const char* _trace_trx_id_index_prefix = "trace_trx_idx_"; static constexpr const char* _trace_ext = ".log"; static constexpr const char* _compressed_trace_ext = ".clog"; - static constexpr int _max_filename_size = std::char_traits::length(_trace_index_prefix) + 10 + 1 + 10 + std::char_traits::length(_compressed_trace_ext) + 1; // "trace_index_" + 10-digits + '-' + 10-digits + ".clog" + null-char + // longest prefix is "trace_trx_idx_" (14), then 10+1+10 digits, then ".clog" extension, then null + static constexpr int _max_filename_size = std::char_traits::length(_trace_trx_id_index_prefix) + 10 + 1 + 10 + std::char_traits::length(_compressed_trace_ext) + 1; std::string make_filename(const char* slice_prefix, const char* slice_ext, uint32_t slice_number, uint32_t slice_width) { char filename[_max_filename_size] = {}; @@ -98,11 +101,26 @@ namespace sysio::trace_api { } get_block_n store_provider::get_trx_block_number(const chain::transaction_id_type& trx_id, const yield_function& yield) { - // traversing from last stride to first - // if we find a trx it is either LIB or it is the latest fork, either way we are done + // Fast path: probe the per-slice hash index for each slice newest-to-oldest. + // The index covers only irreversible slices; reversible blocks fall through to + // the linear scan below. std::set trx_block_nums; _slice_directory.for_each_trx_id_slice([&](fc::cfile& trx_id_file) -> bool { + yield(); + + // Derive the slice number from the file path and try the index first. + const uint32_t slice_number = _slice_directory.slice_number_from_path(trx_id_file.get_file_path()); + if (auto reader = _slice_directory.find_trx_id_index_slice(slice_number)) { + if (auto block_num = reader->lookup(trx_id)) { + trx_block_nums.insert(*block_num); + return false; // found in an irreversible slice; stop + } + return true; // not in this indexed slice; continue to next + } + + // No index for this slice (reversible window or index not yet built): + // fall back to linear scan. metadata_log_entry entry; auto ds = trx_id_file.create_datastream(); const uint64_t end = file_size(trx_id_file.get_file_path()); @@ -120,21 +138,18 @@ namespace sysio::trace_api { break; } } - // block can be seen again when a fork happens, if not in the new block remove it from blocks that have the trx if (!found_in_block) trx_block_nums.erase(trxs_entry.block_num); } else if (std::holds_alternative(entry)) { auto lib = std::get(entry).lib; if (!trx_block_nums.empty() && lib >= *(--trx_block_nums.end())) { - return false; // *(--trx_block_nums.end()) is the block with highest block number which is final + return false; } } else { - FC_ASSERT( false, "unpacked data should be a block_trxs_entry or a lib_entry_v0" );; + FC_ASSERT(false, "unpacked data should be a block_trxs_entry or a lib_entry_v0"); } offset = trx_id_file.tellp(); } - // if empty() keep searching - // if not empty() then we have found the trx and since traversing in reverse order this should be the latest return trx_block_nums.empty(); }); @@ -383,6 +398,60 @@ namespace sysio::trace_api { return _slice_directory.last_recorded_block(); } + uint32_t slice_directory::slice_number_from_path(const std::filesystem::path& trx_id_path) const { + // Filename format: trace_trx_id_XXXXXXXXXX-YYYYYYYYYY.log + // Parse the start block number (XXXXXXXXXX) and divide by _width. + const auto name = trx_id_path.filename().string(); + const auto prefix_len = std::char_traits::length(_trace_trx_id_prefix); + const uint32_t start_block = static_cast(std::stoul(name.substr(prefix_len, 10))); + return start_block / _width; + } + + std::optional slice_directory::find_trx_id_index_slice(uint32_t slice_number) const { + auto filename = make_filename(_trace_trx_id_index_prefix, _trace_ext, slice_number, _width); + const auto path = _slice_dir / filename; + if (!std::filesystem::exists(path)) + return std::nullopt; + trx_id_index_reader reader(path); + if (!reader.valid()) + return std::nullopt; + return reader; + } + + void slice_directory::build_trx_id_index(uint32_t slice_number, const log_handler& log) { + auto idx_filename = make_filename(_trace_trx_id_index_prefix, _trace_ext, slice_number, _width); + const auto idx_path = _slice_dir / idx_filename; + if (std::filesystem::exists(idx_path)) + return; // already built + + fc::cfile trx_id_file; + if (!find_trx_id_slice(slice_number, open_state::read, trx_id_file)) + return; // no source data + + log(std::string("Building trx_id index for slice: ") + std::to_string(slice_number)); + + trx_id_index_writer writer; + const uint64_t end = file_size(trx_id_file.get_file_path()); + while (trx_id_file.tellp() < end) { + metadata_log_entry entry; + auto ds = trx_id_file.create_datastream(); + fc::raw::unpack(ds, entry); + if (std::holds_alternative(entry)) { + const auto& te = std::get(entry); + for (const auto& id : te.ids) + writer.add(id, te.block_num); + } + } + + // Write to a temp path and atomically rename so concurrent readers never + // see a partially written index file. + const auto tmp_path = idx_path.parent_path() / (idx_path.filename().string() + ".tmp"); + writer.write(tmp_path); + std::filesystem::rename(tmp_path, idx_path); + log(std::string("Built trx_id index for slice: ") + std::to_string(slice_number) + + " (" + std::to_string(writer.entry_count()) + " entries)"); + } + void slice_directory::set_lib(uint32_t lib) { { std::scoped_lock lock(_maintenance_mtx); @@ -448,6 +517,14 @@ namespace sysio::trace_api { } void slice_directory::run_maintenance_tasks(uint32_t lib, const log_handler& log) { + // Build trx_id indexes for all newly irreversible slices (min_irreversible=0: + // index as soon as a slice's block range is fully below LIB). + process_irreversible_slice_range(lib, 0, _last_indexed_slice, [this, &log](uint32_t slice_to_index){ + try { + build_trx_id_index(slice_to_index, log); + } FC_LOG_AND_DROP(); + }); + if (_minimum_irreversible_history_blocks) { process_irreversible_slice_range(lib, *_minimum_irreversible_history_blocks, _last_cleaned_up_slice, [this, &log](uint32_t slice_to_clean){ fc::cfile trace; @@ -473,6 +550,12 @@ namespace sysio::trace_api { log(std::string("Removing: ") + trx_id.get_file_path().generic_string()); std::filesystem::remove(trx_id.get_file_path()); } + auto idx_filename = make_filename(_trace_trx_id_index_prefix, _trace_ext, slice_to_clean, _width); + const auto idx_path = _slice_dir / idx_filename; + if (std::filesystem::exists(idx_path)) { + log(std::string("Removing: ") + idx_path.generic_string()); + std::filesystem::remove(idx_path); + } auto ctrace = find_compressed_trace_slice(slice_to_clean, dont_open_file); if (ctrace) { diff --git a/plugins/trace_api_plugin/src/trx_id_index.cpp b/plugins/trace_api_plugin/src/trx_id_index.cpp new file mode 100644 index 0000000000..10f229d22b --- /dev/null +++ b/plugins/trace_api_plugin/src/trx_id_index.cpp @@ -0,0 +1,115 @@ +#include +#include + +#include +#include + +#include +#include + +namespace sysio::trace_api { + +uint64_t trx_id_index_writer::prefix_of(const chain::transaction_id_type& id) { + // transaction_id_type is fc::sha256 — use the first 8 bytes as the hash key. + // fc::sha256 is a plain struct with no padding before its data. + uint64_t p; + static_assert(sizeof(chain::transaction_id_type) >= sizeof(p)); + std::memcpy(&p, &id, sizeof(p)); + return p; +} + +void trx_id_index_writer::add(const chain::transaction_id_type& trx_id, uint32_t block_num) { + _entries.emplace_back(prefix_of(trx_id), block_num); +} + +void trx_id_index_writer::write(const std::filesystem::path& path) const { + // Target load factor <= 0.5; bucket_count is a power of two for fast modulo. + const uint32_t n = static_cast(_entries.size()); + const uint32_t bucket_count = std::max(4u, std::bit_ceil(n * 2 + 1)); + const uint32_t mask = bucket_count - 1; + + std::vector buckets(bucket_count); // zero-initialized = all empty + + for (const auto& [prefix, block_num] : _entries) { + uint32_t idx = static_cast(prefix) & mask; + while (buckets[idx].block_num != 0) { + idx = (idx + 1) & mask; + } + buckets[idx].prefix64 = prefix; + buckets[idx].block_num = block_num; + } + + fc::cfile f; + f.set_file_path(path); + f.open(fc::cfile::create_or_update_rw_mode); + + trx_id_index_header header; + header.bucket_count = bucket_count; + auto hdr_data = fc::raw::pack(header); + f.write(hdr_data.data(), hdr_data.size()); + + for (const auto& b : buckets) { + auto bkt_data = fc::raw::pack(b); + f.write(bkt_data.data(), bkt_data.size()); + } + f.flush(); +} + +// --------------------------------------------------------------------------- + +uint64_t trx_id_index_reader::prefix_of(const chain::transaction_id_type& id) { + uint64_t p; + std::memcpy(&p, &id, sizeof(p)); + return p; +} + +trx_id_index_reader::trx_id_index_reader(const std::filesystem::path& path) { + try { + fc::cfile f; + f.set_file_path(path); + f.open("rb"); + f.seek(0); + + const auto header = extract_store(f); + if (header.magic != trx_id_index_header::magic_value) { + wlog("trace_api: trx_id index {} has wrong magic, ignoring", path.generic_string()); + return; + } + if (header.version != trx_id_index_header::current_version) { + wlog("trace_api: trx_id index {} has unsupported version {}, ignoring", + path.generic_string(), header.version); + return; + } + if (header.bucket_count == 0) { + _valid = true; + return; + } + + _buckets.resize(header.bucket_count); + for (auto& b : _buckets) { + auto ds = f.create_datastream(); + fc::raw::unpack(ds, b); + } + _valid = true; + } catch (...) { + wlog("trace_api: failed to load trx_id index from {}", path.generic_string()); + } +} + +std::optional trx_id_index_reader::lookup(const chain::transaction_id_type& trx_id) const { + if (!_valid || _buckets.empty()) + return std::nullopt; + + const uint64_t prefix = prefix_of(trx_id); + const uint32_t mask = static_cast(_buckets.size()) - 1; + uint32_t idx = static_cast(prefix) & mask; + + while (_buckets[idx].block_num != 0) { + if (_buckets[idx].prefix64 == prefix) + return _buckets[idx].block_num; + idx = (idx + 1) & mask; + } + return std::nullopt; +} + +} // namespace sysio::trace_api diff --git a/plugins/trace_api_plugin/test/CMakeLists.txt b/plugins/trace_api_plugin/test/CMakeLists.txt index 6b75f0f303..c74de4701d 100644 --- a/plugins/trace_api_plugin/test/CMakeLists.txt +++ b/plugins/trace_api_plugin/test/CMakeLists.txt @@ -6,6 +6,7 @@ add_executable( test_trace_api_plugin test_data_handlers.cpp test_configuration_utils.cpp test_compressed_file.cpp + test_trx_id_index.cpp main.cpp ) target_link_libraries( test_trace_api_plugin trace_api_plugin ) diff --git a/plugins/trace_api_plugin/test/test_trx_id_index.cpp b/plugins/trace_api_plugin/test/test_trx_id_index.cpp new file mode 100644 index 0000000000..0ecd7116bb --- /dev/null +++ b/plugins/trace_api_plugin/test/test_trx_id_index.cpp @@ -0,0 +1,232 @@ +#include +#include +#include +#include + +#include +#include + +using namespace sysio; +using namespace sysio::trace_api; +using namespace sysio::trace_api::test_common; + +namespace { + +// Build a sha256 whose first 8 bytes (the prefix64) equal the given value. +// On little-endian x86 this is the little-endian encoding written into bytes 0-7. +chain::transaction_id_type make_trx_id(uint64_t prefix64, uint8_t disambiguator = 0) { + chain::transaction_id_type id; + std::memcpy(id.data(), &prefix64, sizeof(prefix64)); + // vary the last byte so ids with the same prefix64 are still distinct objects + id.data()[31] = disambiguator; + return id; +} + +struct trx_id_index_fixture { + fc::temp_directory tempdir; + + std::filesystem::path index_path() const { + return tempdir.path() / "test_trx_idx.log"; + } +}; + +} // namespace + +BOOST_AUTO_TEST_SUITE(trx_id_index_tests) + +// --------------------------------------------------------------------------- +// Writer / Reader round-trip +// --------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE(empty_writer_is_valid, trx_id_index_fixture) { + trx_id_index_writer w; + BOOST_REQUIRE_EQUAL(w.entry_count(), 0u); + w.write(index_path()); + + trx_id_index_reader r(index_path()); + BOOST_CHECK(r.valid()); + // empty index: any lookup should return nullopt + BOOST_CHECK(!r.lookup(make_trx_id(0))); + BOOST_CHECK(!r.lookup(make_trx_id(42))); +} + +BOOST_FIXTURE_TEST_CASE(single_entry_round_trip, trx_id_index_fixture) { + auto id = make_trx_id(0xDEADBEEFCAFEBABEULL); + const uint32_t block = 12345; + + trx_id_index_writer w; + w.add(id, block); + w.write(index_path()); + + trx_id_index_reader r(index_path()); + BOOST_REQUIRE(r.valid()); + auto result = r.lookup(id); + BOOST_REQUIRE(result.has_value()); + BOOST_CHECK_EQUAL(*result, block); +} + +BOOST_FIXTURE_TEST_CASE(multiple_entries_round_trip, trx_id_index_fixture) { + // 20 distinct entries with distinct prefix64 values, varied block numbers + const int N = 20; + std::vector> entries; + trx_id_index_writer w; + for (int i = 0; i < N; ++i) { + auto id = make_trx_id(static_cast(i) * 0x0101010101010101ULL + i, static_cast(i)); + uint32_t block = 1000 + i; + w.add(id, block); + entries.emplace_back(id, block); + } + BOOST_REQUIRE_EQUAL(w.entry_count(), static_cast(N)); + w.write(index_path()); + + trx_id_index_reader r(index_path()); + BOOST_REQUIRE(r.valid()); + for (const auto& [id, expected_block] : entries) { + auto result = r.lookup(id); + BOOST_REQUIRE_MESSAGE(result.has_value(), "entry not found for block " << expected_block); + BOOST_CHECK_EQUAL(*result, expected_block); + } +} + +BOOST_FIXTURE_TEST_CASE(not_found_returns_nullopt, trx_id_index_fixture) { + trx_id_index_writer w; + w.add(make_trx_id(0xAAAAAAAAAAAAAAAAULL), 100); + w.write(index_path()); + + trx_id_index_reader r(index_path()); + BOOST_REQUIRE(r.valid()); + // id not in the index + BOOST_CHECK(!r.lookup(make_trx_id(0xBBBBBBBBBBBBBBBBULL))); +} + +// --------------------------------------------------------------------------- +// Linear probing: two entries that hash to the same initial slot +// --------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE(linear_probing_on_prefix_collision, trx_id_index_fixture) { + // With 2 entries, bucket_count = bit_ceil(2*2+1) = bit_ceil(5) = 8, mask = 7. + // Both ids below have uint32_t(prefix64) & 7 == 0, so they start at slot 0. + // The second must be linearly probed to slot 1. + const uint64_t prefix_A = 0x0000000000000000ULL; // slot = 0 & 7 = 0 + const uint64_t prefix_B = 0x0000000000000008ULL; // slot = 8 & 7 = 0 + auto id_A = make_trx_id(prefix_A, 1); + auto id_B = make_trx_id(prefix_B, 2); + + trx_id_index_writer w; + w.add(id_A, 101); + w.add(id_B, 202); + w.write(index_path()); + + trx_id_index_reader r(index_path()); + BOOST_REQUIRE(r.valid()); + + auto result_A = r.lookup(id_A); + BOOST_REQUIRE(result_A.has_value()); + BOOST_CHECK_EQUAL(*result_A, 101u); + + auto result_B = r.lookup(id_B); + BOOST_REQUIRE(result_B.has_value()); + BOOST_CHECK_EQUAL(*result_B, 202u); +} + +// --------------------------------------------------------------------------- +// Last-write-wins for duplicate prefix64 +// --------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE(duplicate_prefix64_last_write_wins, trx_id_index_fixture) { + // The writer can accumulate two entries with the same prefix64 (e.g., same + // first-8 bytes). The hash table stores both under different slots. The + // reader returns the *first* hit during linear probing — which is the + // first-inserted entry. Document this behavior so it doesn't surprise. + const uint64_t shared_prefix = 0xCAFEBABEDEADBEEFULL; + auto id1 = make_trx_id(shared_prefix, 1); + auto id2 = make_trx_id(shared_prefix, 2); // same prefix64, different tail + + trx_id_index_writer w; + w.add(id1, 10); + w.add(id2, 20); // will probe to next empty slot + w.write(index_path()); + + trx_id_index_reader r(index_path()); + BOOST_REQUIRE(r.valid()); + // Lookup finds first match on the shared prefix; result is either 10 or 20. + // The important thing is it doesn't crash or loop infinitely. + auto result = r.lookup(id1); + BOOST_CHECK(result.has_value()); +} + +// --------------------------------------------------------------------------- +// Error paths +// --------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE(missing_file_is_invalid, trx_id_index_fixture) { + trx_id_index_reader r(tempdir.path() / "nonexistent.log"); + BOOST_CHECK(!r.valid()); +} + +BOOST_FIXTURE_TEST_CASE(bad_magic_is_invalid, trx_id_index_fixture) { + // Write a file with a bad magic value + fc::cfile f; + f.set_file_path(index_path()); + f.open(fc::cfile::create_or_update_rw_mode); + trx_id_index_header hdr; + hdr.magic = 0xDEADBEEF; // wrong magic + hdr.version = trx_id_index_header::current_version; + hdr.bucket_count = 0; + auto data = fc::raw::pack(hdr); + f.write(data.data(), data.size()); + f.flush(); + f.close(); + + trx_id_index_reader r(index_path()); + BOOST_CHECK(!r.valid()); +} + +BOOST_FIXTURE_TEST_CASE(bad_version_is_invalid, trx_id_index_fixture) { + fc::cfile f; + f.set_file_path(index_path()); + f.open(fc::cfile::create_or_update_rw_mode); + trx_id_index_header hdr; + hdr.magic = trx_id_index_header::magic_value; + hdr.version = 99; // unsupported version + hdr.bucket_count = 0; + auto data = fc::raw::pack(hdr); + f.write(data.data(), data.size()); + f.flush(); + f.close(); + + trx_id_index_reader r(index_path()); + BOOST_CHECK(!r.valid()); +} + +// --------------------------------------------------------------------------- +// Load factor / large table +// --------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE(large_entry_set_all_found, trx_id_index_fixture) { + // Write 200 entries. bucket_count = bit_ceil(201*2) = bit_ceil(401) = 512. + // Load factor = 200/512 ~ 0.39 — well under 0.5. + const int N = 200; + std::vector> entries; + trx_id_index_writer w; + for (int i = 0; i < N; ++i) { + // Spread prefix64 values across the full range + uint64_t prefix = static_cast(i) * 0x9E3779B97F4A7C15ULL; // Fibonacci hashing + auto id = make_trx_id(prefix, static_cast(i & 0xFF)); + uint32_t block = 100000 + static_cast(i); + w.add(id, block); + entries.emplace_back(id, block); + } + BOOST_REQUIRE_EQUAL(w.entry_count(), static_cast(N)); + w.write(index_path()); + + trx_id_index_reader r(index_path()); + BOOST_REQUIRE(r.valid()); + for (const auto& [id, expected_block] : entries) { + auto result = r.lookup(id); + BOOST_REQUIRE_MESSAGE(result.has_value(), "entry not found for block " << expected_block); + BOOST_CHECK_EQUAL(*result, expected_block); + } +} + +BOOST_AUTO_TEST_SUITE_END() From 45c96a43f8dec11c5d687d61d0c7d5cadba32dbb Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Mon, 13 Apr 2026 15:09:18 -0500 Subject: [PATCH 03/32] trace_api: capture and persist ABIs from chain, remove --trace-rpc-abi/--trace-no-abis Replace the file-based --trace-rpc-abi option with automatic ABI capture: - abi_store: sorted on-disk index mapping (account, global_seq) -> ABI bytes, enabling O(log n) point-in-time lookups per action. Written atomically via temp-file rename; loaded at startup for continuity across restarts. - chain_extraction: lazy-fetches each new account's ABI via find_account_metadata on first observation, and captures setabi transactions in real time to track ABI version changes at the exact global_sequence where they took effect. - abi_data_handler: replaced static add_abi() map with an abi_lookup_fn callback that performs versioned (account, global_seq) lookups from the ABI store. Decoding failures are now soft (log at debug, fall back to raw hex) since ABIs are auto-captured rather than operator-provided. - Removed --trace-rpc-abi and --trace-no-abis options from nodeop, all integration tests, TestHarness, config templates, tools, tutorials, and the examples/abis/ directory of hand-maintained ABI files. --- etc/config/nodeop/aio/config.template.ini | 1 - .../trace_api_plugin/examples/abis/sysio.abi | 918 ------------------ .../examples/abis/sysio.msig.abi | 290 ------ .../examples/abis/sysio.token.abi | 184 ---- .../examples/abis/sysio.wrap.abi | 105 -- .../sysio/trace_api/abi_data_handler.hpp | 41 +- .../include/sysio/trace_api/abi_store.hpp | 97 ++ .../sysio/trace_api/chain_extraction.hpp | 53 +- .../sysio/trace_api/store_provider.hpp | 31 +- .../trace_api_plugin/src/abi_data_handler.cpp | 70 +- plugins/trace_api_plugin/src/abi_store.cpp | 188 ++++ .../trace_api_plugin/src/store_provider.cpp | 20 +- .../trace_api_plugin/src/trace_api_plugin.cpp | 82 +- plugins/trace_api_plugin/test/CMakeLists.txt | 1 + .../trace_api_plugin/test/test_abi_store.cpp | 247 +++++ .../test/test_data_handlers.cpp | 34 +- .../trace_api_plugin/test/test_extraction.cpp | 4 + tests/TestHarness/Cluster.py | 2 - tests/TestHarness/launcher.py | 3 +- tests/cli_test.py | 2 +- tests/nodeop_lib_test.py | 3 +- tests/nodeop_run_test.py | 3 +- tests/plugin_http_api_test.py | 2 +- tests/resource_monitor_plugin_test.py | 4 +- tests/trace_plugin_test.py | 3 +- tools/cluster_manager.py | 1 - .../bios-boot-tutorial/bios-boot-tutorial.py | 2 +- 27 files changed, 771 insertions(+), 1620 deletions(-) delete mode 100644 plugins/trace_api_plugin/examples/abis/sysio.abi delete mode 100644 plugins/trace_api_plugin/examples/abis/sysio.msig.abi delete mode 100644 plugins/trace_api_plugin/examples/abis/sysio.token.abi delete mode 100644 plugins/trace_api_plugin/examples/abis/sysio.wrap.abi create mode 100644 plugins/trace_api_plugin/include/sysio/trace_api/abi_store.hpp create mode 100644 plugins/trace_api_plugin/src/abi_store.cpp create mode 100644 plugins/trace_api_plugin/test/test_abi_store.cpp diff --git a/etc/config/nodeop/aio/config.template.ini b/etc/config/nodeop/aio/config.template.ini index 5b2b223d3e..33aeb5f645 100644 --- a/etc/config/nodeop/aio/config.template.ini +++ b/etc/config/nodeop/aio/config.template.ini @@ -46,5 +46,4 @@ p2p-server-address = 127.0.0.1:4444 max-clients = 150 # Trace API Plugin -trace-no-abis = true # Use to indicate that the RPC responses will not use ABIs. ( NOTE: REMOVE THIS LINE IF YOU WANT TO USE ABIs IN RPC RESPONSES) trace-minimum-irreversible-history-blocks = -1 # Number of blocks to ensure are kept past LIB for retrieval before "slice" files can be automatically removed. A value of -1 indicates that automatic removal of "slice" files will be turned off. diff --git a/plugins/trace_api_plugin/examples/abis/sysio.abi b/plugins/trace_api_plugin/examples/abis/sysio.abi deleted file mode 100644 index 1f9bd87fbe..0000000000 --- a/plugins/trace_api_plugin/examples/abis/sysio.abi +++ /dev/null @@ -1,918 +0,0 @@ -{ - "____comment": "This file was generated with sysio-abigen. DO NOT EDIT ", - "version": "sysio::abi/1.2", - "types": [ - { - "new_type_name": "block_signing_authority", - "type": "variant_block_signing_authority_v0" - }, - { - "new_type_name": "blockchain_parameters_t", - "type": "blockchain_parameters" - } - ], - "structs": [ - { - "name": "abi_hash", - "base": "", - "fields": [ - { - "name": "owner", - "type": "name" - }, - { - "name": "hash", - "type": "checksum256" - } - ] - }, - { - "name": "activate", - "base": "", - "fields": [ - { - "name": "feature_digest", - "type": "checksum256" - } - ] - }, - { - "name": "authority", - "base": "", - "fields": [ - { - "name": "threshold", - "type": "uint32" - }, - { - "name": "keys", - "type": "key_weight[]" - }, - { - "name": "accounts", - "type": "permission_level_weight[]" - } - ] - }, - { - "name": "block_header", - "base": "", - "fields": [ - { - "name": "timestamp", - "type": "uint32" - }, - { - "name": "producer", - "type": "name" - }, - { - "name": "confirmed", - "type": "uint16" - }, - { - "name": "previous", - "type": "checksum256" - }, - { - "name": "transaction_mroot", - "type": "checksum256" - }, - { - "name": "action_mroot", - "type": "checksum256" - }, - { - "name": "schedule_version", - "type": "uint32" - }, - { - "name": "new_producers", - "type": "producer_schedule?" - } - ] - }, - { - "name": "block_info_record", - "base": "", - "fields": [ - { - "name": "version", - "type": "uint8" - }, - { - "name": "block_height", - "type": "uint32" - }, - { - "name": "block_timestamp", - "type": "time_point" - } - ] - }, - { - "name": "block_signing_authority_v0", - "base": "", - "fields": [ - { - "name": "threshold", - "type": "uint32" - }, - { - "name": "keys", - "type": "key_weight[]" - } - ] - }, - { - "name": "blockchain_parameters", - "base": "", - "fields": [ - { - "name": "max_block_net_usage", - "type": "uint64" - }, - { - "name": "target_block_net_usage_pct", - "type": "uint32" - }, - { - "name": "max_transaction_net_usage", - "type": "uint32" - }, - { - "name": "base_per_transaction_net_usage", - "type": "uint32" - }, - { - "name": "net_usage_leeway", - "type": "uint32" - }, - { - "name": "context_free_discount_net_usage_num", - "type": "uint32" - }, - { - "name": "context_free_discount_net_usage_den", - "type": "uint32" - }, - { - "name": "max_block_cpu_usage", - "type": "uint32" - }, - { - "name": "target_block_cpu_usage_pct", - "type": "uint32" - }, - { - "name": "max_transaction_cpu_usage", - "type": "uint32" - }, - { - "name": "min_transaction_cpu_usage", - "type": "uint32" - }, - { - "name": "max_transaction_lifetime", - "type": "uint32" - }, - { - "name": "deferred_trx_expiration_window", - "type": "uint32" - }, - { - "name": "max_transaction_delay", - "type": "uint32" - }, - { - "name": "max_inline_action_size", - "type": "uint32" - }, - { - "name": "max_inline_action_depth", - "type": "uint16" - }, - { - "name": "max_authority_depth", - "type": "uint16" - } - ] - }, - { - "name": "deleteauth", - "base": "", - "fields": [ - { - "name": "account", - "type": "name" - }, - { - "name": "permission", - "type": "name" - }, - { - "name": "authorized_by", - "type": "name$" - } - ] - }, - { - "name": "init", - "base": "", - "fields": [ - { - "name": "version", - "type": "varuint32" - }, - { - "name": "core", - "type": "symbol" - } - ] - }, - { - "name": "key_weight", - "base": "", - "fields": [ - { - "name": "key", - "type": "public_key" - }, - { - "name": "weight", - "type": "uint16" - } - ] - }, - { - "name": "limitauthchg", - "base": "", - "fields": [ - { - "name": "account", - "type": "name" - }, - { - "name": "allow_perms", - "type": "name[]" - }, - { - "name": "disallow_perms", - "type": "name[]" - } - ] - }, - { - "name": "linkauth", - "base": "", - "fields": [ - { - "name": "account", - "type": "name" - }, - { - "name": "code", - "type": "name" - }, - { - "name": "type", - "type": "name" - }, - { - "name": "requirement", - "type": "name" - }, - { - "name": "authorized_by", - "type": "name$" - } - ] - }, - { - "name": "newaccount", - "base": "", - "fields": [ - { - "name": "creator", - "type": "name" - }, - { - "name": "name", - "type": "name" - }, - { - "name": "owner", - "type": "authority" - }, - { - "name": "active", - "type": "authority" - } - ] - }, - { - "name": "onblock", - "base": "", - "fields": [ - { - "name": "header", - "type": "block_header" - } - ] - }, - { - "name": "permission_level", - "base": "", - "fields": [ - { - "name": "actor", - "type": "name" - }, - { - "name": "permission", - "type": "name" - } - ] - }, - { - "name": "permission_level_weight", - "base": "", - "fields": [ - { - "name": "permission", - "type": "permission_level" - }, - { - "name": "weight", - "type": "uint16" - } - ] - }, - { - "name": "producer_authority", - "base": "", - "fields": [ - { - "name": "producer_name", - "type": "name" - }, - { - "name": "authority", - "type": "block_signing_authority" - } - ] - }, - { - "name": "producer_info", - "base": "", - "fields": [ - { - "name": "owner", - "type": "name" - }, - { - "name": "producer_key", - "type": "public_key" - }, - { - "name": "is_active", - "type": "bool" - }, - { - "name": "url", - "type": "string" - }, - { - "name": "unpaid_blocks", - "type": "uint32" - }, - { - "name": "last_claim_time", - "type": "time_point" - }, - { - "name": "location", - "type": "uint16" - }, - { - "name": "producer_authority", - "type": "block_signing_authority" - } - ] - }, - { - "name": "producer_key", - "base": "", - "fields": [ - { - "name": "producer_name", - "type": "name" - }, - { - "name": "block_signing_key", - "type": "public_key" - } - ] - }, - { - "name": "producer_schedule", - "base": "", - "fields": [ - { - "name": "version", - "type": "uint32" - }, - { - "name": "producers", - "type": "producer_key[]" - } - ] - }, - { - "name": "regproducer", - "base": "", - "fields": [ - { - "name": "producer", - "type": "name" - }, - { - "name": "producer_key", - "type": "public_key" - }, - { - "name": "url", - "type": "string" - }, - { - "name": "location", - "type": "uint16" - } - ] - }, - { - "name": "regproducer2", - "base": "", - "fields": [ - { - "name": "producer", - "type": "name" - }, - { - "name": "producer_authority", - "type": "block_signing_authority" - }, - { - "name": "url", - "type": "string" - }, - { - "name": "location", - "type": "uint16" - } - ] - }, - { - "name": "rmvproducer", - "base": "", - "fields": [ - { - "name": "producer", - "type": "name" - } - ] - }, - { - "name": "setabi", - "base": "", - "fields": [ - { - "name": "account", - "type": "name" - }, - { - "name": "abi", - "type": "bytes" - }, - { - "name": "memo", - "type": "string$" - } - ] - }, - { - "name": "setacctcpu", - "base": "", - "fields": [ - { - "name": "account", - "type": "name" - }, - { - "name": "cpu_weight", - "type": "int64?" - } - ] - }, - { - "name": "setacctnet", - "base": "", - "fields": [ - { - "name": "account", - "type": "name" - }, - { - "name": "net_weight", - "type": "int64?" - } - ] - }, - { - "name": "setacctram", - "base": "", - "fields": [ - { - "name": "account", - "type": "name" - }, - { - "name": "ram_bytes", - "type": "int64?" - } - ] - }, - { - "name": "setalimits", - "base": "", - "fields": [ - { - "name": "account", - "type": "name" - }, - { - "name": "ram_bytes", - "type": "int64" - }, - { - "name": "net_weight", - "type": "int64" - }, - { - "name": "cpu_weight", - "type": "int64" - } - ] - }, - { - "name": "setcode", - "base": "", - "fields": [ - { - "name": "account", - "type": "name" - }, - { - "name": "vmtype", - "type": "uint8" - }, - { - "name": "vmversion", - "type": "uint8" - }, - { - "name": "code", - "type": "bytes" - }, - { - "name": "memo", - "type": "string$" - } - ] - }, - { - "name": "setparams", - "base": "", - "fields": [ - { - "name": "params", - "type": "blockchain_parameters_t" - } - ] - }, - { - "name": "setpriv", - "base": "", - "fields": [ - { - "name": "account", - "type": "name" - }, - { - "name": "is_priv", - "type": "uint8" - } - ] - }, - { - "name": "setprods", - "base": "", - "fields": [ - { - "name": "schedule", - "type": "producer_authority[]" - } - ] - }, - { - "name": "setram", - "base": "", - "fields": [ - { - "name": "max_ram_size", - "type": "uint64" - } - ] - }, - { - "name": "sysio_global_state", - "base": "blockchain_parameters", - "fields": [ - { - "name": "max_ram_size", - "type": "uint64" - }, - { - "name": "total_ram_bytes_reserved", - "type": "uint64" - }, - { - "name": "last_producer_schedule_update", - "type": "block_timestamp_type" - }, - { - "name": "last_pervote_bucket_fill", - "type": "time_point" - }, - { - "name": "total_unpaid_blocks", - "type": "uint32" - }, - { - "name": "total_activated_stake", - "type": "int64" - }, - { - "name": "thresh_activated_stake_time", - "type": "time_point" - }, - { - "name": "last_producer_schedule_size", - "type": "uint16" - } - ] - }, - { - "name": "unlinkauth", - "base": "", - "fields": [ - { - "name": "account", - "type": "name" - }, - { - "name": "code", - "type": "name" - }, - { - "name": "type", - "type": "name" - }, - { - "name": "authorized_by", - "type": "name$" - } - ] - }, - { - "name": "unregprod", - "base": "", - "fields": [ - { - "name": "producer", - "type": "name" - } - ] - }, - { - "name": "updateauth", - "base": "", - "fields": [ - { - "name": "account", - "type": "name" - }, - { - "name": "permission", - "type": "name" - }, - { - "name": "parent", - "type": "name" - }, - { - "name": "auth", - "type": "authority" - }, - { - "name": "authorized_by", - "type": "name$" - } - ] - }, - { - "name": "limit_auth_change", - "base": "", - "fields": [ - { - "name": "version", - "type": "uint8" - }, - { - "name": "account", - "type": "name" - }, - { - "name": "allow_perms", - "type": "name[]" - }, - { - "name": "disallow_perms", - "type": "name[]" - } - ] - } - ], - "actions": [ - { - "name": "activate", - "type": "activate", - "ricardian_contract": "" - }, - { - "name": "deleteauth", - "type": "deleteauth", - "ricardian_contract": "" - }, - { - "name": "init", - "type": "init", - "ricardian_contract": "" - }, - { - "name": "limitauthchg", - "type": "limitauthchg", - "ricardian_contract": "" - }, - { - "name": "linkauth", - "type": "linkauth", - "ricardian_contract": "" - }, - { - "name": "newaccount", - "type": "newaccount", - "ricardian_contract": "" - }, - { - "name": "onblock", - "type": "onblock", - "ricardian_contract": "" - }, - { - "name": "regproducer", - "type": "regproducer", - "ricardian_contract": "" - }, - { - "name": "regproducer2", - "type": "regproducer2", - "ricardian_contract": "" - }, - { - "name": "rmvproducer", - "type": "rmvproducer", - "ricardian_contract": "" - }, - { - "name": "setabi", - "type": "setabi", - "ricardian_contract": "" - }, - { - "name": "setacctcpu", - "type": "setacctcpu", - "ricardian_contract": "" - }, - { - "name": "setacctnet", - "type": "setacctnet", - "ricardian_contract": "" - }, - { - "name": "setacctram", - "type": "setacctram", - "ricardian_contract": "" - }, - { - "name": "setalimits", - "type": "setalimits", - "ricardian_contract": "" - }, - { - "name": "setcode", - "type": "setcode", - "ricardian_contract": "" - }, - { - "name": "setparams", - "type": "setparams", - "ricardian_contract": "" - }, - { - "name": "setpriv", - "type": "setpriv", - "ricardian_contract": "" - }, - { - "name": "setprods", - "type": "setprods", - "ricardian_contract": "" - }, - { - "name": "setram", - "type": "setram", - "ricardian_contract": "" - }, - { - "name": "unlinkauth", - "type": "unlinkauth", - "ricardian_contract": "" - }, - { - "name": "unregprod", - "type": "unregprod", - "ricardian_contract": "" - }, - { - "name": "updateauth", - "type": "updateauth", - "ricardian_contract": "" - } - ], - "tables": [ - { - "name": "abihash", - "type": "abi_hash", - "index_type": "i64", - "key_names": [], - "key_types": [] - }, - { - "name": "blockinfo", - "type": "block_info_record", - "index_type": "i64", - "key_names": [], - "key_types": [] - }, - { - "name": "global", - "type": "sysio_global_state", - "index_type": "i64", - "key_names": [], - "key_types": [] - }, - { - "name": "producers", - "type": "producer_info", - "index_type": "i64", - "key_names": [], - "key_types": [] - }, - { - "name": "limitauthchg", - "type": "limit_auth_change", - "index_type": "i64", - "key_names": [], - "key_types": [] - } - ], - "ricardian_clauses": [], - "variants": [ - { - "name": "variant_block_signing_authority_v0", - "types": ["block_signing_authority_v0"] - } - ], - "action_results": [] -} \ No newline at end of file diff --git a/plugins/trace_api_plugin/examples/abis/sysio.msig.abi b/plugins/trace_api_plugin/examples/abis/sysio.msig.abi deleted file mode 100644 index 36b6b12d36..0000000000 --- a/plugins/trace_api_plugin/examples/abis/sysio.msig.abi +++ /dev/null @@ -1,290 +0,0 @@ -{ - "version": "sysio::abi/1.2", - "types": [], - "structs": [{ - "name": "action", - "base": "", - "fields": [{ - "name": "account", - "type": "name" - },{ - "name": "name", - "type": "name" - },{ - "name": "authorization", - "type": "permission_level[]" - },{ - "name": "data", - "type": "bytes" - } - ] - },{ - "name": "approval", - "base": "", - "fields": [{ - "name": "level", - "type": "permission_level" - },{ - "name": "time", - "type": "time_point" - } - ] - },{ - "name": "approvals_info", - "base": "", - "fields": [{ - "name": "version", - "type": "uint8" - },{ - "name": "proposal_name", - "type": "name" - },{ - "name": "requested_approvals", - "type": "approval[]" - },{ - "name": "provided_approvals", - "type": "approval[]" - } - ] - },{ - "name": "approve", - "base": "", - "fields": [{ - "name": "proposer", - "type": "name" - },{ - "name": "proposal_name", - "type": "name" - },{ - "name": "level", - "type": "permission_level" - },{ - "name": "proposal_hash", - "type": "checksum256$" - } - ] - },{ - "name": "cancel", - "base": "", - "fields": [{ - "name": "proposer", - "type": "name" - },{ - "name": "proposal_name", - "type": "name" - },{ - "name": "canceler", - "type": "name" - } - ] - },{ - "name": "exec", - "base": "", - "fields": [{ - "name": "proposer", - "type": "name" - },{ - "name": "proposal_name", - "type": "name" - },{ - "name": "executer", - "type": "name" - } - ] - },{ - "name": "extension", - "base": "", - "fields": [{ - "name": "type", - "type": "uint16" - },{ - "name": "data", - "type": "bytes" - } - ] - },{ - "name": "invalidate", - "base": "", - "fields": [{ - "name": "account", - "type": "name" - } - ] - },{ - "name": "invalidation", - "base": "", - "fields": [{ - "name": "account", - "type": "name" - },{ - "name": "last_invalidation_time", - "type": "time_point" - } - ] - },{ - "name": "old_approvals_info", - "base": "", - "fields": [{ - "name": "proposal_name", - "type": "name" - },{ - "name": "requested_approvals", - "type": "permission_level[]" - },{ - "name": "provided_approvals", - "type": "permission_level[]" - } - ] - },{ - "name": "permission_level", - "base": "", - "fields": [{ - "name": "actor", - "type": "name" - },{ - "name": "permission", - "type": "name" - } - ] - },{ - "name": "proposal", - "base": "", - "fields": [{ - "name": "proposal_name", - "type": "name" - },{ - "name": "packed_transaction", - "type": "bytes" - },{ - "name": "earliest_exec_time", - "type": "time_point?$" - } - ] - },{ - "name": "propose", - "base": "", - "fields": [{ - "name": "proposer", - "type": "name" - },{ - "name": "proposal_name", - "type": "name" - },{ - "name": "requested", - "type": "permission_level[]" - },{ - "name": "trx", - "type": "transaction" - } - ] - },{ - "name": "transaction", - "base": "transaction_header", - "fields": [{ - "name": "context_free_actions", - "type": "action[]" - },{ - "name": "actions", - "type": "action[]" - },{ - "name": "transaction_extensions", - "type": "extension[]" - } - ] - },{ - "name": "transaction_header", - "base": "", - "fields": [{ - "name": "expiration", - "type": "time_point_sec" - },{ - "name": "ref_block_num", - "type": "uint16" - },{ - "name": "ref_block_prefix", - "type": "uint32" - },{ - "name": "max_net_usage_words", - "type": "varuint32" - },{ - "name": "max_cpu_usage_ms", - "type": "uint8" - },{ - "name": "delay_sec", - "type": "varuint32" - } - ] - },{ - "name": "unapprove", - "base": "", - "fields": [{ - "name": "proposer", - "type": "name" - },{ - "name": "proposal_name", - "type": "name" - },{ - "name": "level", - "type": "permission_level" - } - ] - } - ], - "actions": [{ - "name": "approve", - "type": "approve", - "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Approve Proposed Transaction\nsummary: '{{nowrap level.actor}} approves the {{nowrap proposal_name}} proposal'\nicon: https://raw.githubusercontent.com/eosnetworkfoundation/eos-system-contracts/main/contracts/icons/multisig.png#4fb41d3cf02d0dd2d35a29308e93c2d826ec770d6bb520db668f530764be7153\n---\n\n{{level.actor}} approves the {{proposal_name}} proposal proposed by {{proposer}} with the {{level.permission}} permission of {{level.actor}}." - },{ - "name": "cancel", - "type": "cancel", - "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Cancel Proposed Transaction\nsummary: '{{nowrap canceler}} cancels the {{nowrap proposal_name}} proposal'\nicon: https://raw.githubusercontent.com/eosnetworkfoundation/eos-system-contracts/main/contracts/icons/multisig.png#4fb41d3cf02d0dd2d35a29308e93c2d826ec770d6bb520db668f530764be7153\n---\n\n{{canceler}} cancels the {{proposal_name}} proposal submitted by {{proposer}}." - },{ - "name": "exec", - "type": "exec", - "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Execute Proposed Transaction\nsummary: '{{nowrap executer}} executes the {{nowrap proposal_name}} proposal'\nicon: https://raw.githubusercontent.com/eosnetworkfoundation/eos-system-contracts/main/contracts/icons/multisig.png#4fb41d3cf02d0dd2d35a29308e93c2d826ec770d6bb520db668f530764be7153\n---\n\n{{executer}} executes the {{proposal_name}} proposal submitted by {{proposer}} if the minimum required approvals for the proposal have been secured." - },{ - "name": "invalidate", - "type": "invalidate", - "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Invalidate All Approvals\nsummary: '{{nowrap account}} invalidates approvals on outstanding proposals'\nicon: https://raw.githubusercontent.com/eosnetworkfoundation/eos-system-contracts/main/contracts/icons/multisig.png#4fb41d3cf02d0dd2d35a29308e93c2d826ec770d6bb520db668f530764be7153\n---\n\n{{account}} invalidates all approvals on proposals which have not yet executed." - },{ - "name": "propose", - "type": "propose", - "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Propose Transaction\nsummary: '{{nowrap proposer}} creates the {{nowrap proposal_name}}'\nicon: https://raw.githubusercontent.com/eosnetworkfoundation/eos-system-contracts/main/contracts/icons/multisig.png#4fb41d3cf02d0dd2d35a29308e93c2d826ec770d6bb520db668f530764be7153\n---\n\n{{proposer}} creates the {{proposal_name}} proposal for the following transaction:\n{{to_json trx}}\n\nThe proposal requests approvals from the following accounts at the specified permission levels:\n{{#each requested}}\n + {{this.permission}} permission of {{this.actor}}\n{{/each}}\n\nIf the proposed transaction is not executed prior to {{trx.expiration}}, the proposal will automatically expire." - },{ - "name": "unapprove", - "type": "unapprove", - "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Unapprove Proposed Transaction\nsummary: '{{nowrap level.actor}} revokes the approval previously provided to {{nowrap proposal_name}} proposal'\nicon: https://raw.githubusercontent.com/eosnetworkfoundation/eos-system-contracts/main/contracts/icons/multisig.png#4fb41d3cf02d0dd2d35a29308e93c2d826ec770d6bb520db668f530764be7153\n---\n\n{{level.actor}} revokes the approval previously provided at their {{level.permission}} permission level from the {{proposal_name}} proposal proposed by {{proposer}}." - } - ], - "tables": [{ - "name": "approvals", - "index_type": "i64", - "key_names": [], - "key_types": [], - "type": "old_approvals_info" - },{ - "name": "approvals2", - "index_type": "i64", - "key_names": [], - "key_types": [], - "type": "approvals_info" - },{ - "name": "invals", - "index_type": "i64", - "key_names": [], - "key_types": [], - "type": "invalidation" - },{ - "name": "proposal", - "index_type": "i64", - "key_names": [], - "key_types": [], - "type": "proposal" - } - ], - "ricardian_clauses": [], - "error_messages": [], - "abi_extensions": [], - "variants": [], - "action_results": [] -} diff --git a/plugins/trace_api_plugin/examples/abis/sysio.token.abi b/plugins/trace_api_plugin/examples/abis/sysio.token.abi deleted file mode 100644 index da098d8d5c..0000000000 --- a/plugins/trace_api_plugin/examples/abis/sysio.token.abi +++ /dev/null @@ -1,184 +0,0 @@ -{ - "version": "sysio::abi/1.2", - "types": [], - "structs": [{ - "name": "account", - "base": "", - "fields": [{ - "name": "balance", - "type": "asset" - } - ] - },{ - "name": "close", - "base": "", - "fields": [{ - "name": "owner", - "type": "name" - },{ - "name": "symbol", - "type": "symbol" - } - ] - },{ - "name": "create", - "base": "", - "fields": [{ - "name": "issuer", - "type": "name" - },{ - "name": "maximum_supply", - "type": "asset" - } - ] - },{ - "name": "currency_stats", - "base": "", - "fields": [{ - "name": "supply", - "type": "asset" - },{ - "name": "max_supply", - "type": "asset" - },{ - "name": "issuer", - "type": "name" - } - ] - },{ - "name": "issue", - "base": "", - "fields": [{ - "name": "to", - "type": "name" - },{ - "name": "quantity", - "type": "asset" - },{ - "name": "memo", - "type": "string" - } - ] - },{ - "name": "issuefixed", - "base": "", - "fields": [{ - "name": "to", - "type": "name" - },{ - "name": "supply", - "type": "asset" - },{ - "name": "memo", - "type": "string" - } - ] - },{ - "name": "open", - "base": "", - "fields": [{ - "name": "owner", - "type": "name" - },{ - "name": "symbol", - "type": "symbol" - },{ - "name": "ram_payer", - "type": "name" - } - ] - },{ - "name": "retire", - "base": "", - "fields": [{ - "name": "quantity", - "type": "asset" - },{ - "name": "memo", - "type": "string" - } - ] - },{ - "name": "setmaxsupply", - "base": "", - "fields": [{ - "name": "issuer", - "type": "name" - },{ - "name": "maximum_supply", - "type": "asset" - } - ] - },{ - "name": "transfer", - "base": "", - "fields": [{ - "name": "from", - "type": "name" - },{ - "name": "to", - "type": "name" - },{ - "name": "quantity", - "type": "asset" - },{ - "name": "memo", - "type": "string" - } - ] - } - ], - "actions": [{ - "name": "close", - "type": "close", - "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Close Token Balance\nsummary: 'Close {{nowrap owner}}’s zero quantity balance'\nicon: https://raw.githubusercontent.com/eosnetworkfoundation/eos-system-contracts/main/contracts/icons/token.png#207ff68b0406eaa56618b08bda81d6a0954543f36adc328ab3065f31a5c5d654\n---\n\n{{owner}} agrees to close their zero quantity balance for the {{symbol_to_symbol_code symbol}} token.\n\nRAM will be refunded to the RAM payer of the {{symbol_to_symbol_code symbol}} token balance for {{owner}}." - },{ - "name": "create", - "type": "create", - "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Create New Token\nsummary: 'Create a new token'\nicon: https://raw.githubusercontent.com/eosnetworkfoundation/eos-system-contracts/main/contracts/icons/token.png#207ff68b0406eaa56618b08bda81d6a0954543f36adc328ab3065f31a5c5d654\n---\n\n{{$action.account}} agrees to create a new token with symbol {{asset_to_symbol_code maximum_supply}} to be managed by {{issuer}}.\n\nThis action will not result any any tokens being issued into circulation.\n\n{{issuer}} will be allowed to issue tokens into circulation, up to a maximum supply of {{maximum_supply}}.\n\nRAM will deducted from {{$action.account}}’s resources to create the necessary records." - },{ - "name": "issue", - "type": "issue", - "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Issue Tokens into Circulation\nsummary: 'Issue {{nowrap quantity}} into circulation and transfer into {{nowrap to}}’s account'\nicon: https://raw.githubusercontent.com/eosnetworkfoundation/eos-system-contracts/main/contracts/icons/token.png#207ff68b0406eaa56618b08bda81d6a0954543f36adc328ab3065f31a5c5d654\n---\n\nThe token manager agrees to issue {{quantity}} into circulation, and transfer it into {{to}}’s account.\n\n{{#if memo}}There is a memo attached to the transfer stating:\n{{memo}}\n{{/if}}\n\nIf {{to}} does not have a balance for {{asset_to_symbol_code quantity}}, or the token manager does not have a balance for {{asset_to_symbol_code quantity}}, the token manager will be designated as the RAM payer of the {{asset_to_symbol_code quantity}} token balance for {{to}}. As a result, RAM will be deducted from the token manager’s resources to create the necessary records.\n\nThis action does not allow the total quantity to exceed the max allowed supply of the token." - },{ - "name": "issuefixed", - "type": "issuefixed", - "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Issue Fixed Supply of Tokens into Circulation\nsummary: 'Issue up to {{nowrap supply}} supply into circulation and transfer into {{nowrap to}}’s account'\nicon: https://raw.githubusercontent.com/eosnetworkfoundation/eos-system-contracts/main/contracts/icons/token.png#207ff68b0406eaa56618b08bda81d6a0954543f36adc328ab3065f31a5c5d654\n---\n\nThe token manager agrees to issue tokens up to {{supply}} fixed supply into circulation, and transfer it into {{to}}’s account.\n\n{{#if memo}}There is a memo attached to the transfer stating:\n{{memo}}\n{{/if}}\n\nIf {{to}} does not have a balance for {{asset_to_symbol_code quantity}}, or the token manager does not have a balance for {{asset_to_symbol_code quantity}}, the token manager will be designated as the RAM payer of the {{asset_to_symbol_code quantity}} token balance for {{to}}. As a result, RAM will be deducted from the token manager’s resources to create the necessary records.\n\nThis action does not allow the total quantity to exceed the max allowed supply of the token." - },{ - "name": "open", - "type": "open", - "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Open Token Balance\nsummary: 'Open a zero quantity balance for {{nowrap owner}}'\nicon: https://raw.githubusercontent.com/eosnetworkfoundation/eos-system-contracts/main/contracts/icons/token.png#207ff68b0406eaa56618b08bda81d6a0954543f36adc328ab3065f31a5c5d654\n---\n\n{{ram_payer}} agrees to establish a zero quantity balance for {{owner}} for the {{symbol_to_symbol_code symbol}} token.\n\nIf {{owner}} does not have a balance for {{symbol_to_symbol_code symbol}}, {{ram_payer}} will be designated as the RAM payer of the {{symbol_to_symbol_code symbol}} token balance for {{owner}}. As a result, RAM will be deducted from {{ram_payer}}’s resources to create the necessary records." - },{ - "name": "retire", - "type": "retire", - "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Remove Tokens from Circulation\nsummary: 'Remove {{nowrap quantity}} from circulation'\nicon: https://raw.githubusercontent.com/eosnetworkfoundation/eos-system-contracts/main/contracts/icons/token.png#207ff68b0406eaa56618b08bda81d6a0954543f36adc328ab3065f31a5c5d654\n---\n\nThe token manager agrees to remove {{quantity}} from circulation, taken from their own account.\n\n{{#if memo}} There is a memo attached to the action stating:\n{{memo}}\n{{/if}}" - },{ - "name": "setmaxsupply", - "type": "setmaxsupply", - "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Set Max Supply\nsummary: 'Set max supply for token'\nicon: https://raw.githubusercontent.com/eosnetworkfoundation/eos-system-contracts/main/contracts/icons/token.png#207ff68b0406eaa56618b08bda81d6a0954543f36adc328ab3065f31a5c5d654\n---\n\n{{issuer}} will be allowed to issue tokens into circulation, up to a maximum supply of {{maximum_supply}}.\n\nThis action will not result any any tokens being issued into circulation." - },{ - "name": "transfer", - "type": "transfer", - "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Transfer Tokens\nsummary: 'Send {{nowrap quantity}} from {{nowrap from}} to {{nowrap to}}'\nicon: https://raw.githubusercontent.com/eosnetworkfoundation/eos-system-contracts/main/contracts/icons/transfer.png#5dfad0df72772ee1ccc155e670c1d124f5c5122f1d5027565df38b418042d1dd\n---\n\n{{from}} agrees to send {{quantity}} to {{to}}.\n\n{{#if memo}}There is a memo attached to the transfer stating:\n{{memo}}\n{{/if}}\n\nIf {{from}} is not already the RAM payer of their {{asset_to_symbol_code quantity}} token balance, {{from}} will be designated as such. As a result, RAM will be deducted from {{from}}’s resources to refund the original RAM payer.\n\nIf {{to}} does not have a balance for {{asset_to_symbol_code quantity}}, {{from}} will be designated as the RAM payer of the {{asset_to_symbol_code quantity}} token balance for {{to}}. As a result, RAM will be deducted from {{from}}’s resources to create the necessary records." - } - ], - "tables": [{ - "name": "accounts", - "index_type": "i64", - "key_names": [], - "key_types": [], - "type": "account" - },{ - "name": "stat", - "index_type": "i64", - "key_names": [], - "key_types": [], - "type": "currency_stats" - } - ], - "ricardian_clauses": [], - "error_messages": [], - "abi_extensions": [], - "variants": [], - "action_results": [] -} diff --git a/plugins/trace_api_plugin/examples/abis/sysio.wrap.abi b/plugins/trace_api_plugin/examples/abis/sysio.wrap.abi deleted file mode 100644 index 4326f6e57c..0000000000 --- a/plugins/trace_api_plugin/examples/abis/sysio.wrap.abi +++ /dev/null @@ -1,105 +0,0 @@ -{ - "version": "sysio::abi/1.2", - "types": [], - "structs": [{ - "name": "action", - "base": "", - "fields": [{ - "name": "account", - "type": "name" - },{ - "name": "name", - "type": "name" - },{ - "name": "authorization", - "type": "permission_level[]" - },{ - "name": "data", - "type": "bytes" - } - ] - },{ - "name": "exec", - "base": "", - "fields": [{ - "name": "executer", - "type": "name" - },{ - "name": "trx", - "type": "transaction" - } - ] - },{ - "name": "extension", - "base": "", - "fields": [{ - "name": "type", - "type": "uint16" - },{ - "name": "data", - "type": "bytes" - } - ] - },{ - "name": "permission_level", - "base": "", - "fields": [{ - "name": "actor", - "type": "name" - },{ - "name": "permission", - "type": "name" - } - ] - },{ - "name": "transaction", - "base": "transaction_header", - "fields": [{ - "name": "context_free_actions", - "type": "action[]" - },{ - "name": "actions", - "type": "action[]" - },{ - "name": "transaction_extensions", - "type": "extension[]" - } - ] - },{ - "name": "transaction_header", - "base": "", - "fields": [{ - "name": "expiration", - "type": "time_point_sec" - },{ - "name": "ref_block_num", - "type": "uint16" - },{ - "name": "ref_block_prefix", - "type": "uint32" - },{ - "name": "max_net_usage_words", - "type": "varuint32" - },{ - "name": "max_cpu_usage_ms", - "type": "uint8" - },{ - "name": "delay_sec", - "type": "varuint32" - } - ] - } - ], - "actions": [{ - "name": "exec", - "type": "exec", - "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Privileged Execute\nsummary: '{{nowrap executer}} executes a transaction while bypassing authority checks'\nicon: https://raw.githubusercontent.com/eosnetworkfoundation/eos-system-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\n{{executer}} executes the following transaction while bypassing authority checks:\n{{to_json trx}}\n\n{{$action.account}} must also authorize this action." - } - ], - "tables": [], - "ricardian_clauses": [], - "error_messages": [], - "abi_extensions": [], - "variants": [], - "action_results": [] -} diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp index 5def5b8ac4..588a588bd8 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp @@ -1,6 +1,10 @@ #pragma once +#include +#include +#include #include +#include #include #include @@ -12,29 +16,34 @@ namespace sysio { namespace trace_api { /** - * Data Handler that uses sysio::chain::abi_serializer to decode data with a known set of ABI's - * Can be used directly as a Data_handler_provider OR shared between request_handlers using the - * ::shared_provider abstraction. + * Data Handler that uses sysio::chain::abi_serializer to decode action data. + * + * ABIs are resolved dynamically via an abi_lookup_fn callback, typically backed + * by the abi_store on disk. Given (account, global_sequence) it returns the raw + * ABI bytes that were in effect at that point in chain history, or nullopt. + * + * Can be used directly as a Data_handler_provider OR shared between request_handlers + * using the ::shared_provider abstraction. */ class abi_data_handler { public: - explicit abi_data_handler( exception_handler except_handler ) - :except_handler( std::move( except_handler ) ) - { - } + /// Callback: (account, global_sequence) -> raw ABI bytes in effect at that sequence, or nullopt. + /// Called on the HTTP thread; must be thread-safe. + using abi_lookup_fn = std::function>(chain::name, uint64_t)>; - /** - * Add an ABI definition to this data handler - * @param name - the name of the account/contract that this ABI belongs to - * @param abi - the ABI definition of that ABI - */ - void add_abi( const chain::name& name, chain::abi_def&& abi ); + explicit abi_data_handler( exception_handler except_handler, abi_lookup_fn lookup_fn = {} ) + :_abi_lookup_fn( std::move( lookup_fn ) ) + ,except_handler( std::move( except_handler ) ) + {} /** - * Given an action trace, produce a tuple representing the `data` and `return_value` fields in the trace + * Given an action trace, produce a tuple representing the `data` and `return_value` fields + * in the trace, decoded via the ABI in effect at that action's global_sequence. * * @param action - trace of the action including metadata necessary for finding the ABI - * @return tuple where the first element is a variant representing the `data` field of the action interpreted by known ABIs OR an empty variant, and the second element represents the `return_value` field of the trace. + * @return tuple where the first element is a variant representing the decoded `data` field, + * and the second element represents the decoded `return_value` field. + * Both are empty variants when the ABI is unavailable or decoding fails. */ std::tuple> serialize_to_variant(const std::variant& action); @@ -55,7 +64,7 @@ namespace sysio { }; private: - std::map> abi_serializer_by_account; + abi_lookup_fn _abi_lookup_fn; exception_handler except_handler; }; } } diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/abi_store.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/abi_store.hpp new file mode 100644 index 0000000000..1880d76d11 --- /dev/null +++ b/plugins/trace_api_plugin/include/sysio/trace_api/abi_store.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace sysio::trace_api { + +// --------------------------------------------------------------------------- +// On-disk format: abi_store.log +// +// Single-file layout (written atomically via .tmp + rename): +// +// Header (16 bytes): magic, version, entry_count, reserved +// Index (entry_count * 24 bytes, sorted by account ASC, global_seq ASC): +// account(8) + global_seq(8) + blob_offset(4) + blob_size(4) +// blob_offset is relative to the start of the blob area. +// Blob area (variable): raw ABI bytes concatenated in index order +// +// Serialized with fc::raw (native-endian), consistent with other trace slice files. +// +// To find the ABI for account A in effect at global_seq Q: +// Binary-search the index for the last entry where account==A && global_seq<=Q, +// then read blob_size bytes from (blob_area_start + blob_offset) in the file. +// --------------------------------------------------------------------------- + +struct abi_store_header { + static constexpr uint32_t magic_value = 0x41424942; // "ABIB" + static constexpr uint32_t current_version = 1; + + uint32_t magic = magic_value; + uint32_t version = current_version; + uint32_t entry_count = 0; + uint32_t _reserved = 0; +}; +static_assert(sizeof(abi_store_header) == 16); + +struct abi_store_index_entry { + uint64_t account; // chain::name value + uint64_t global_seq; + uint32_t blob_offset; // relative to blob area start + uint32_t blob_size; +}; +static_assert(sizeof(abi_store_index_entry) == 24); + +// --------------------------------------------------------------------------- +// Writer: accumulate (account, global_seq, abi_bytes) triples; write atomically. +// If the same (account, global_seq) key is added twice, last-write-wins. +// --------------------------------------------------------------------------- + +class abi_store_writer { +public: + void add(chain::name account, uint64_t global_seq, std::vector abi_bytes); + void write(const std::filesystem::path& path) const; + // Pre-populate from an existing abi_store.log so previously captured ABIs survive + // across node restarts and are included in the next write(). + void load(const std::filesystem::path& path); + size_t entry_count() const { return _entries.size(); } + +private: + struct entry { + uint64_t account; + uint64_t global_seq; + std::vector abi_bytes; + }; + std::vector _entries; +}; + +// --------------------------------------------------------------------------- +// Reader: load the index from disk, answer (account, global_seq) lookups. +// The index is held in memory; ABI bytes are read from the file on demand. +// --------------------------------------------------------------------------- + +class abi_store_reader { +public: + explicit abi_store_reader(const std::filesystem::path& path); + + bool valid() const { return _valid; } + + // Returns the ABI bytes in effect for account at global_seq (i.e., the ABI + // with the largest global_seq <= the query), or nullopt if none is found. + std::optional> lookup(chain::name account, uint64_t global_seq) const; + +private: + std::filesystem::path _path; + std::vector _index; // sorted by (account, global_seq) + uint64_t _blob_area_offset{0}; + bool _valid{false}; +}; + +} // namespace sysio::trace_api + +FC_REFLECT(sysio::trace_api::abi_store_header, (magic)(version)(entry_count)(_reserved)) +FC_REFLECT(sysio::trace_api::abi_store_index_entry, (account)(global_seq)(blob_offset)(blob_size)) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp index 7fff78c658..6fc38adce5 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp @@ -1,12 +1,15 @@ #pragma once #include -#include #include +#include +#include +#include #include #include #include #include +#include namespace sysio { namespace trace_api { @@ -16,14 +19,24 @@ using chain::packed_transaction; template class chain_extraction_impl_type { public: + /** + * Called to fetch the current ABI bytes for an account (lazy init on first encounter). + * Returns nullopt if the account has no ABI. + */ + using abi_fetcher_t = std::function>(chain::name)>; + /** * Chain Extractor for capturing transaction traces, action traces, and block info. - * @param store provider of append & append_lib + * @param store provider of append, append_lib, and append_abi * @param except_handler called on exceptions, logging if any is left to the user + * @param abi_fetcher optional callback to lazily fetch the current ABI for an account; + * called on first encounter of each account; receives global_seq 0 */ - chain_extraction_impl_type( StoreProvider store, exception_handler except_handler ) + chain_extraction_impl_type( StoreProvider store, exception_handler except_handler, + abi_fetcher_t abi_fetcher = {} ) : store(std::move(store)) , except_handler(std::move(except_handler)) + , _abi_fetcher(std::move(abi_fetcher)) {} /// connect to chain controller applied_transaction signal @@ -55,6 +68,37 @@ class chain_extraction_impl_type { } else { cached_traces[trace->id] = {trace, t}; } + + // ABI capture: scan all action traces (including inlines) in this transaction. + for (const auto& at : trace->action_traces) { + if (!at.receipt) continue; // skip context-free or failed actions + + // Lazy ABI fetch: on first encounter of any account, record its current ABI at + // global_seq 0 ("captured before tracing began; exact version unknown"). + if (_abi_fetcher && _seen_accounts.insert(at.act.account.to_uint64_t()).second) { + try { + if (auto abi = _abi_fetcher(at.act.account)) + store.append_abi(at.act.account, 0, std::move(*abi)); + } catch (...) {} + } + + // setabi: record the new ABI with its exact global_sequence. + if (at.act.account == chain::config::system_account_name && + at.act.name == chain::name("setabi")) { + try { + chain::name target_account; + chain::bytes abi_bytes; + auto ds = fc::datastream(at.act.data.data(), at.act.data.size()); + fc::raw::unpack(ds, target_account); + fc::raw::unpack(ds, abi_bytes); + store.append_abi(target_account, + at.receipt->global_sequence, + std::vector(abi_bytes.begin(), abi_bytes.end())); + // Mark target as seen so lazy fetch doesn't later overwrite with a stale entry. + _seen_accounts.insert(target_account.to_uint64_t()); + } catch (...) {} + } + } } void on_accepted_block(const chain::signed_block_ptr& block, const chain::block_id_type& id ) { @@ -153,8 +197,11 @@ class chain_extraction_impl_type { private: StoreProvider store; exception_handler except_handler; + abi_fetcher_t _abi_fetcher; std::map cached_traces; std::optional onblock_trace; + // Track seen accounts (by raw uint64 name value) to avoid redundant lazy ABI fetches. + std::unordered_set _seen_accounts; bool _startup_checked{false}; }; diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp index a47ce1690b..b9cec45f08 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp @@ -1,15 +1,17 @@ #pragma once #include -#include +#include #include +#include #include #include #include +#include #include -#include -#include #include +#include +#include #include namespace sysio::trace_api { @@ -308,6 +310,20 @@ namespace sysio::trace_api { void append_lib(uint32_t lib); void append_trx_ids(block_trxs_entry tt); + /** + * Record an ABI version for an account at a given global_sequence. + * global_seq == 0 means "captured lazily; exact seq unknown". + * Thread-safe; may be called from the extraction thread. + */ + void append_abi(chain::name account, uint64_t global_seq, std::vector abi_bytes); + + /** + * Return the ABI bytes in effect for account at global_seq (the ABI with the + * largest recorded global_seq <= the query), or nullopt if none is found. + * Thread-safe; may be called from the HTTP thread. + */ + std::optional> lookup_abi(chain::name account, uint64_t global_seq) const; + /** * Read the trace for a given block * @param block_height : the height of the data being read @@ -422,6 +438,15 @@ namespace sysio::trace_api { void validate_existing_index_slice_file(fc::cfile& index, open_state state); slice_directory _slice_directory; + + private: + // ABI sidecar: one global file in the slice directory. + // _abi_write_mutex serialises writes (extraction thread). + // _abi_reader is atomically swapped after each write for lock-free reads (HTTP thread). + mutable std::mutex _abi_write_mutex; + abi_store_writer _abi_writer; + std::atomic> _abi_reader; + std::filesystem::path _abi_store_path; }; } diff --git a/plugins/trace_api_plugin/src/abi_data_handler.cpp b/plugins/trace_api_plugin/src/abi_data_handler.cpp index 338e665aa8..34980ba7bc 100644 --- a/plugins/trace_api_plugin/src/abi_data_handler.cpp +++ b/plugins/trace_api_plugin/src/abi_data_handler.cpp @@ -1,42 +1,54 @@ #include #include +#include namespace sysio::trace_api { - void abi_data_handler::add_abi( const chain::name& name, chain::abi_def&& abi ) { - // currently abis are operator provided so no need to protect against abuse - abi_serializer_by_account.emplace(name, - std::make_shared(std::move(abi), chain::abi_serializer::create_yield_function(fc::microseconds::maximum()))); - } - std::tuple> abi_data_handler::serialize_to_variant(const std::variant& action) { return std::visit([&](const auto& a) -> std::tuple> { - if (abi_serializer_by_account.count(a.account) > 0) { - const auto &serializer_p = abi_serializer_by_account.at(a.account); - auto type_name = serializer_p->get_action_type(a.action); - - if (!type_name.empty()) { - try { - // abi_serializer expects a yield function that takes a recursion depth - // abis are user provided, do not use a deadline - auto abi_yield = [](size_t recursion_depth) { - SYS_ASSERT( recursion_depth < chain::abi_serializer::max_recursion_depth, chain::abi_recursion_depth_exception, - "exceeded max_recursion_depth {} ", chain::abi_serializer::max_recursion_depth ); - }; - std::optional ret_data; - auto params = serializer_p->binary_to_variant(type_name, a.data, abi_yield); - if(a.return_value.size() > 0) { - auto return_type_name = serializer_p->get_action_result_type(a.action); - if (!return_type_name.empty()) { - ret_data = serializer_p->binary_to_variant(return_type_name, a.return_value, abi_yield); - } - } - return {std::move(params), std::move(ret_data)}; - } catch (...) { - except_handler(MAKE_EXCEPTION_WITH_CONTEXT(std::current_exception())); + if (!_abi_lookup_fn) { + return {}; + } + + auto abi_bytes = _abi_lookup_fn(a.account, a.global_sequence); + if (!abi_bytes || abi_bytes->empty()) { + return {}; + } + + try { + chain::abi_def abi; + auto ds = fc::datastream(abi_bytes->data(), abi_bytes->size()); + fc::raw::unpack(ds, abi); + + chain::abi_serializer serializer(std::move(abi), + chain::abi_serializer::create_yield_function(fc::microseconds::maximum())); + + auto type_name = serializer.get_action_type(a.action); + if (type_name.empty()) { + return {}; + } + + // abi_serializer expects a yield function that takes a recursion depth + // abis are user provided, do not use a deadline + auto abi_yield = [](size_t recursion_depth) { + SYS_ASSERT( recursion_depth < chain::abi_serializer::max_recursion_depth, + chain::abi_recursion_depth_exception, + "exceeded max_recursion_depth {} ", chain::abi_serializer::max_recursion_depth ); + }; + + std::optional ret_data; + auto params = serializer.binary_to_variant(type_name, a.data, abi_yield); + if (a.return_value.size() > 0) { + auto return_type_name = serializer.get_action_result_type(a.action); + if (!return_type_name.empty()) { + ret_data = serializer.binary_to_variant(return_type_name, a.return_value, abi_yield); } } + return {std::move(params), std::move(ret_data)}; + } catch (...) { + except_handler(MAKE_EXCEPTION_WITH_CONTEXT(std::current_exception())); } + return {}; }, action); } diff --git a/plugins/trace_api_plugin/src/abi_store.cpp b/plugins/trace_api_plugin/src/abi_store.cpp new file mode 100644 index 0000000000..fc17db0724 --- /dev/null +++ b/plugins/trace_api_plugin/src/abi_store.cpp @@ -0,0 +1,188 @@ +#include + +#include +#include + +#include +#include + +namespace sysio::trace_api { + +// --------------------------------------------------------------------------- +// abi_store_writer +// --------------------------------------------------------------------------- + +void abi_store_writer::add(chain::name account, uint64_t global_seq, std::vector abi_bytes) { + _entries.push_back({account.to_uint64_t(), global_seq, std::move(abi_bytes)}); +} + +void abi_store_writer::write(const std::filesystem::path& path) const { + // Sort by (account, global_seq); stable so last-add-wins for duplicate keys. + std::vector order(_entries.size()); + std::iota(order.begin(), order.end(), 0); + std::stable_sort(order.begin(), order.end(), [&](size_t a, size_t b) { + if (_entries[a].account != _entries[b].account) + return _entries[a].account < _entries[b].account; + return _entries[a].global_seq < _entries[b].global_seq; + }); + + // Compute blob offsets (relative to blob area start). + uint32_t running_offset = 0; + std::vector blob_offsets(order.size()); + for (size_t i = 0; i < order.size(); ++i) { + blob_offsets[i] = running_offset; + running_offset += static_cast(_entries[order[i]].abi_bytes.size()); + } + + const auto tmp_path = std::filesystem::path(path).replace_extension(".tmp"); + + fc::cfile f; + f.set_file_path(tmp_path); + f.open(fc::cfile::create_or_update_rw_mode); + + // Header + abi_store_header hdr; + hdr.entry_count = static_cast(order.size()); + auto hdr_data = fc::raw::pack(hdr); + f.write(hdr_data.data(), hdr_data.size()); + + // Index (sorted) + for (size_t i = 0; i < order.size(); ++i) { + const auto& e = _entries[order[i]]; + abi_store_index_entry ie; + ie.account = e.account; + ie.global_seq = e.global_seq; + ie.blob_offset = blob_offsets[i]; + ie.blob_size = static_cast(e.abi_bytes.size()); + auto ie_data = fc::raw::pack(ie); + f.write(ie_data.data(), ie_data.size()); + } + + // Blob area (in same sorted order) + for (size_t i : order) { + const auto& blob = _entries[i].abi_bytes; + if (!blob.empty()) + f.write(blob.data(), blob.size()); + } + + f.flush(); + f.close(); + + std::filesystem::rename(tmp_path, path); +} + +// --------------------------------------------------------------------------- +// abi_store_reader +// --------------------------------------------------------------------------- + +abi_store_reader::abi_store_reader(const std::filesystem::path& path) + : _path(path) { + try { + fc::cfile f; + f.set_file_path(path); + f.open("rb"); + f.seek(0); + auto ds = f.create_datastream(); + + abi_store_header hdr; + fc::raw::unpack(ds, hdr); + + if (hdr.magic != abi_store_header::magic_value) { + wlog("trace_api: abi_store {} has wrong magic, ignoring", path.generic_string()); + return; + } + if (hdr.version != abi_store_header::current_version) { + wlog("trace_api: abi_store {} has unsupported version {}, ignoring", + path.generic_string(), hdr.version); + return; + } + + _index.resize(hdr.entry_count); + for (auto& e : _index) + fc::raw::unpack(ds, e); + + // Blob area starts immediately after header + index. + _blob_area_offset = sizeof(abi_store_header) + + static_cast(hdr.entry_count) * sizeof(abi_store_index_entry); + _valid = true; + } catch (...) { + wlog("trace_api: failed to load abi_store from {}", path.generic_string()); + } +} + +std::optional> abi_store_reader::lookup(chain::name account, uint64_t global_seq) const { + if (!_valid || _index.empty()) + return std::nullopt; + + const uint64_t acct = account.to_uint64_t(); + + // upper_bound gives first entry strictly greater than (acct, global_seq). + // Decrement to get the last entry <= the query, then check the account matches. + const abi_store_index_entry key{acct, global_seq, 0, 0}; + auto it = std::upper_bound(_index.begin(), _index.end(), key, + [](const abi_store_index_entry& a, const abi_store_index_entry& b) { + if (a.account != b.account) return a.account < b.account; + return a.global_seq < b.global_seq; + }); + + if (it == _index.begin()) + return std::nullopt; + --it; + if (it->account != acct) + return std::nullopt; + + std::optional> result; + result.emplace(it->blob_size); + try { + if (it->blob_size > 0) { + fc::cfile f; + f.set_file_path(_path); + f.open("rb"); + f.seek(static_cast(_blob_area_offset + it->blob_offset)); + f.read(result->data(), it->blob_size); + } + } catch (...) { + wlog("trace_api: failed to read ABI blob from {}", _path.generic_string()); + return std::nullopt; + } + return result; +} + +// --------------------------------------------------------------------------- +// abi_store_writer::load +// --------------------------------------------------------------------------- + +void abi_store_writer::load(const std::filesystem::path& path) { + try { + fc::cfile f; + f.set_file_path(path); + f.open("rb"); + f.seek(0); + auto ds = f.create_datastream(); + + abi_store_header hdr; + fc::raw::unpack(ds, hdr); + if (hdr.magic != abi_store_header::magic_value) return; + if (hdr.version != abi_store_header::current_version) return; + + std::vector index(hdr.entry_count); + for (auto& e : index) + fc::raw::unpack(ds, e); + + const uint64_t blob_area_start = sizeof(abi_store_header) + + static_cast(hdr.entry_count) * sizeof(abi_store_index_entry); + for (const auto& ie : index) { + std::vector blob(ie.blob_size); + if (ie.blob_size > 0) { + f.seek(static_cast(blob_area_start + ie.blob_offset)); + f.read(blob.data(), ie.blob_size); + } + _entries.push_back({ie.account, ie.global_seq, std::move(blob)}); + } + } catch (...) { + wlog("trace_api: failed to load abi_store into writer from {}", path.generic_string()); + _entries.clear(); + } +} + +} // namespace sysio::trace_api diff --git a/plugins/trace_api_plugin/src/store_provider.cpp b/plugins/trace_api_plugin/src/store_provider.cpp index 0b01477eda..2d6eba1eec 100644 --- a/plugins/trace_api_plugin/src/store_provider.cpp +++ b/plugins/trace_api_plugin/src/store_provider.cpp @@ -35,7 +35,12 @@ namespace { namespace sysio::trace_api { store_provider::store_provider(const std::filesystem::path& slice_dir, uint32_t stride_width, std::optional minimum_irreversible_history_blocks, std::optional minimum_uncompressed_irreversible_history_blocks, size_t compression_seek_point_stride) - : _slice_directory(slice_dir, stride_width, minimum_irreversible_history_blocks, minimum_uncompressed_irreversible_history_blocks, compression_seek_point_stride) { + : _slice_directory(slice_dir, stride_width, minimum_irreversible_history_blocks, minimum_uncompressed_irreversible_history_blocks, compression_seek_point_stride) + , _abi_store_path(slice_dir / "abi_store.log") { + if (std::filesystem::exists(_abi_store_path)) { + _abi_writer.load(_abi_store_path); + _abi_reader.store(std::make_shared(_abi_store_path)); + } } template @@ -398,6 +403,19 @@ namespace sysio::trace_api { return _slice_directory.last_recorded_block(); } + void store_provider::append_abi(chain::name account, uint64_t global_seq, std::vector abi_bytes) { + std::lock_guard lock(_abi_write_mutex); + _abi_writer.add(account, global_seq, std::move(abi_bytes)); + _abi_writer.write(_abi_store_path); + _abi_reader.store(std::make_shared(_abi_store_path)); + } + + std::optional> store_provider::lookup_abi(chain::name account, uint64_t global_seq) const { + auto reader = _abi_reader.load(); + if (!reader) return std::nullopt; + return reader->lookup(account, global_seq); + } + uint32_t slice_directory::slice_number_from_path(const std::filesystem::path& trx_id_path) const { // Filename format: trace_trx_id_XXXXXXXXXX-YYYYYYYYYY.log // Parse the start block number (XXXXXXXXXX) and divide by _width. diff --git a/plugins/trace_api_plugin/src/trace_api_plugin.cpp b/plugins/trace_api_plugin/src/trace_api_plugin.cpp index 74f13319ab..9a8123fc38 100644 --- a/plugins/trace_api_plugin/src/trace_api_plugin.cpp +++ b/plugins/trace_api_plugin/src/trace_api_plugin.cpp @@ -99,6 +99,14 @@ namespace { return store->last_recorded_block(); } + void append_abi(chain::name account, uint64_t global_seq, std::vector abi_bytes) { + store->append_abi(account, global_seq, std::move(abi_bytes)); + } + + std::optional> lookup_abi(chain::name account, uint64_t global_seq) const { + return store->lookup_abi(account, global_seq); + } + std::shared_ptr store; }; } @@ -189,51 +197,21 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_this& common ) :common(common) {} - static void set_program_options(appbase::options_description& cli, appbase::options_description& cfg) { - auto cfg_options = cfg.add_options(); - cfg_options("trace-rpc-abi", bpo::value>()->composing(), - "ABIs used when decoding trace RPC responses.\n" - "There must be at least one ABI specified OR the flag trace-no-abis must be used.\n" - "ABIs are specified as \"Key=Value\" pairs in the form =\n" - "Where can be:\n" - " an absolute path to a file containing a valid JSON-encoded ABI\n" - " a relative path from `data-dir` to a file containing a valid JSON-encoded ABI\n" - ); - cfg_options("trace-no-abis", - "Use to indicate that the RPC responses will not use ABIs.\n" - "Failure to specify this option when there are no trace-rpc-abi configuations will result in an Error.\n" - "This option is mutually exclusive with trace-rpc-api" - ); - } + static void set_program_options(appbase::options_description&, appbase::options_description&) {} - void plugin_initialize(const appbase::variables_map& options) { + void plugin_initialize(const appbase::variables_map&) { ilog("initializing trace api rpc plugin"); - std::shared_ptr data_handler = std::make_shared([](const exception_with_context& e){ - log_exception(e, fc::log_level::debug); - if (std::get<0>(e)) { // rethrow so caller is notified of error - std::rethrow_exception(std::get<0>(e)); + auto store = common->store; + auto data_handler = std::make_shared( + [](const exception_with_context& e) { + // Log at debug and fall back to raw hex — do not rethrow, since + // ABI capture is automatic and decoding failures should be soft. + log_exception(e, fc::log_level::debug); + }, + [store](chain::name account, uint64_t global_seq) { + return store->lookup_abi(account, global_seq); } - }); - - if( options.count("trace-rpc-abi") ) { - SYS_ASSERT(options.count("trace-no-abis") == 0, chain::plugin_config_exception, - "Trace API is configured with ABIs however trace-no-abis is set"); - const std::vector key_value_pairs = options["trace-rpc-abi"].as>(); - for (const auto& entry : key_value_pairs) { - try { - auto kv = parse_kv_pairs(entry); - auto account = chain::name(kv.first); - auto abi = abi_def_from_file(kv.second, app().data_dir()); - data_handler->add_abi(account, std::move(abi)); - } catch (...) { - elog("Malformed trace-rpc-abi provider: \"{}\"", entry); - throw; - } - } - } else { - SYS_ASSERT(options.count("trace-no-abis") != 0, chain::plugin_config_exception, - "Trace API is not configured with ABIs and trace-no-abis is not set"); - } + ); req_handler = std::make_shared( shared_store_provider(common->store), @@ -356,10 +334,26 @@ struct trace_api_plugin_impl { app().quit(); throw yield_exception("shutting down"); }; - extraction = std::make_shared(shared_store_provider(common->store), log_exceptions_and_shutdown); - auto& chain = app().find_plugin()->chain(); + // Lazy ABI fetcher: called from applied_transaction (chain write thread) on first + // encounter of each account. Captures current ABI from the chain DB at that point. + chain_extraction_t::abi_fetcher_t abi_fetcher = [&chain](chain::name account) + -> std::optional> { + std::optional> result; + try { + const auto* meta = chain.find_account_metadata(account); + if (meta && meta->abi.size() > 0) + result.emplace(meta->abi.data(), meta->abi.data() + meta->abi.size()); + } catch (...) {} + return result; + }; + + extraction = std::make_shared( + shared_store_provider(common->store), + log_exceptions_and_shutdown, + std::move(abi_fetcher)); + applied_transaction_connection.emplace( chain.applied_transaction().connect([this](std::tuple t) { emit_killer([&](){ diff --git a/plugins/trace_api_plugin/test/CMakeLists.txt b/plugins/trace_api_plugin/test/CMakeLists.txt index c74de4701d..ff8286b717 100644 --- a/plugins/trace_api_plugin/test/CMakeLists.txt +++ b/plugins/trace_api_plugin/test/CMakeLists.txt @@ -7,6 +7,7 @@ add_executable( test_trace_api_plugin test_configuration_utils.cpp test_compressed_file.cpp test_trx_id_index.cpp + test_abi_store.cpp main.cpp ) target_link_libraries( test_trace_api_plugin trace_api_plugin ) diff --git a/plugins/trace_api_plugin/test/test_abi_store.cpp b/plugins/trace_api_plugin/test/test_abi_store.cpp new file mode 100644 index 0000000000..ac70bef52a --- /dev/null +++ b/plugins/trace_api_plugin/test/test_abi_store.cpp @@ -0,0 +1,247 @@ +#include +#include +#include + +#include +#include + +using namespace sysio; +using namespace sysio::trace_api; +using namespace sysio::trace_api::test_common; + +namespace { + +struct abi_store_fixture { + fc::temp_directory tempdir; + + std::filesystem::path store_path() const { + return tempdir.path() / "test_abi_store.log"; + } + + // Convenience: make a small ABI blob from a string tag + static std::vector make_abi(const std::string& tag) { + return std::vector(tag.begin(), tag.end()); + } +}; + +} // namespace + +BOOST_AUTO_TEST_SUITE(abi_store_tests) + +// --------------------------------------------------------------------------- +// Writer / Reader round-trip +// --------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE(empty_writer_is_valid, abi_store_fixture) { + abi_store_writer w; + BOOST_REQUIRE_EQUAL(w.entry_count(), 0u); + w.write(store_path()); + + abi_store_reader r(store_path()); + BOOST_CHECK(r.valid()); + BOOST_CHECK(!r.lookup("sysio.token"_n, 1)); +} + +BOOST_FIXTURE_TEST_CASE(single_entry_round_trip, abi_store_fixture) { + auto blob = make_abi("abi-v1"); + abi_store_writer w; + w.add("sysio.token"_n, 100, blob); + w.write(store_path()); + + abi_store_reader r(store_path()); + BOOST_REQUIRE(r.valid()); + + auto result = r.lookup("sysio.token"_n, 100); + BOOST_REQUIRE(result.has_value()); + BOOST_CHECK_EQUAL_COLLECTIONS(result->begin(), result->end(), blob.begin(), blob.end()); +} + +BOOST_FIXTURE_TEST_CASE(multiple_accounts_round_trip, abi_store_fixture) { + auto blob1 = make_abi("token-abi-v1"); + auto blob2 = make_abi("eosio-abi-v1"); + abi_store_writer w; + w.add("sysio.token"_n, 50, blob1); + w.add("sysio"_n, 200, blob2); + w.write(store_path()); + + abi_store_reader r(store_path()); + BOOST_REQUIRE(r.valid()); + + auto r1 = r.lookup("sysio.token"_n, 50); + BOOST_REQUIRE(r1.has_value()); + BOOST_CHECK_EQUAL_COLLECTIONS(r1->begin(), r1->end(), blob1.begin(), blob1.end()); + + auto r2 = r.lookup("sysio"_n, 200); + BOOST_REQUIRE(r2.has_value()); + BOOST_CHECK_EQUAL_COLLECTIONS(r2->begin(), r2->end(), blob2.begin(), blob2.end()); +} + +// --------------------------------------------------------------------------- +// ABI version selection (return the version in effect at query global_seq) +// --------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE(returns_abi_in_effect_at_global_seq, abi_store_fixture) { + // Three ABI versions for the same account + auto v1 = make_abi("abi-v1"); + auto v2 = make_abi("abi-v2"); + auto v3 = make_abi("abi-v3"); + + abi_store_writer w; + w.add("sysio.token"_n, 100, v1); + w.add("sysio.token"_n, 200, v2); + w.add("sysio.token"_n, 300, v3); + w.write(store_path()); + + abi_store_reader r(store_path()); + BOOST_REQUIRE(r.valid()); + + // Before any version: not found + BOOST_CHECK(!r.lookup("sysio.token"_n, 99)); + + // Exactly at v1 + auto at100 = r.lookup("sysio.token"_n, 100); + BOOST_REQUIRE(at100.has_value()); + BOOST_CHECK_EQUAL_COLLECTIONS(at100->begin(), at100->end(), v1.begin(), v1.end()); + + // Between v1 and v2: v1 is in effect + auto at150 = r.lookup("sysio.token"_n, 150); + BOOST_REQUIRE(at150.has_value()); + BOOST_CHECK_EQUAL_COLLECTIONS(at150->begin(), at150->end(), v1.begin(), v1.end()); + + // Exactly at v2 + auto at200 = r.lookup("sysio.token"_n, 200); + BOOST_REQUIRE(at200.has_value()); + BOOST_CHECK_EQUAL_COLLECTIONS(at200->begin(), at200->end(), v2.begin(), v2.end()); + + // After v3 + auto at500 = r.lookup("sysio.token"_n, 500); + BOOST_REQUIRE(at500.has_value()); + BOOST_CHECK_EQUAL_COLLECTIONS(at500->begin(), at500->end(), v3.begin(), v3.end()); +} + +BOOST_FIXTURE_TEST_CASE(lookup_wrong_account_returns_nullopt, abi_store_fixture) { + abi_store_writer w; + w.add("sysio.token"_n, 100, make_abi("token-abi")); + w.write(store_path()); + + abi_store_reader r(store_path()); + BOOST_REQUIRE(r.valid()); + BOOST_CHECK(!r.lookup("sysio.msig"_n, 100)); // different account +} + +// --------------------------------------------------------------------------- +// Multiple accounts share the sorted index correctly +// --------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE(accounts_do_not_bleed_into_each_other, abi_store_fixture) { + // sysio.token has v1 at seq=100; sysio has v1 at seq=200. + // Looking up sysio.token at seq=250 must return sysio.token's ABI, not sysio's. + abi_store_writer w; + w.add("sysio.token"_n, 100, make_abi("token-100")); + w.add("sysio"_n, 200, make_abi("sysio-200")); + w.write(store_path()); + + abi_store_reader r(store_path()); + BOOST_REQUIRE(r.valid()); + + auto token_result = r.lookup("sysio.token"_n, 250); + BOOST_REQUIRE(token_result.has_value()); + BOOST_CHECK(*token_result == make_abi("token-100")); + + // sysio.token at seq=50: not found (before first version) + BOOST_CHECK(!r.lookup("sysio.token"_n, 50)); +} + +// --------------------------------------------------------------------------- +// Empty ABI blob (account deleted / cleared ABI) +// --------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE(empty_blob_round_trip, abi_store_fixture) { + abi_store_writer w; + w.add("clearme"_n, 999, {}); // empty ABI + w.write(store_path()); + + abi_store_reader r(store_path()); + BOOST_REQUIRE(r.valid()); + + auto result = r.lookup("clearme"_n, 999); + BOOST_REQUIRE(result.has_value()); + BOOST_CHECK(result->empty()); +} + +// --------------------------------------------------------------------------- +// Error paths +// --------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE(missing_file_is_invalid, abi_store_fixture) { + abi_store_reader r(tempdir.path() / "nonexistent.log"); + BOOST_CHECK(!r.valid()); +} + +BOOST_FIXTURE_TEST_CASE(bad_magic_is_invalid, abi_store_fixture) { + fc::cfile f; + f.set_file_path(store_path()); + f.open(fc::cfile::create_or_update_rw_mode); + abi_store_header hdr; + hdr.magic = 0xDEADBEEF; + auto data = fc::raw::pack(hdr); + f.write(data.data(), data.size()); + f.flush(); + f.close(); + + abi_store_reader r(store_path()); + BOOST_CHECK(!r.valid()); +} + +BOOST_FIXTURE_TEST_CASE(bad_version_is_invalid, abi_store_fixture) { + fc::cfile f; + f.set_file_path(store_path()); + f.open(fc::cfile::create_or_update_rw_mode); + abi_store_header hdr; + hdr.version = 99; + auto data = fc::raw::pack(hdr); + f.write(data.data(), data.size()); + f.flush(); + f.close(); + + abi_store_reader r(store_path()); + BOOST_CHECK(!r.valid()); +} + +// --------------------------------------------------------------------------- +// Many entries +// --------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE(many_accounts_many_versions, abi_store_fixture) { + const int NUM_ACCOUNTS = 10; + const int VERSIONS_PER_ACCOUNT = 5; + + abi_store_writer w; + // Add in reverse order to verify the writer sorts correctly. + for (int a = NUM_ACCOUNTS - 1; a >= 0; --a) { + for (int v = VERSIONS_PER_ACCOUNT - 1; v >= 0; --v) { + auto acct = chain::name(static_cast(a + 1) * 0x10000000000000ULL); + uint64_t seq = static_cast(a * 100 + v * 10 + 1); + w.add(acct, seq, make_abi("a" + std::to_string(a) + "v" + std::to_string(v))); + } + } + BOOST_REQUIRE_EQUAL(w.entry_count(), static_cast(NUM_ACCOUNTS * VERSIONS_PER_ACCOUNT)); + w.write(store_path()); + + abi_store_reader r(store_path()); + BOOST_REQUIRE(r.valid()); + + // Spot-check several (account, global_seq) lookups + for (int a = 0; a < NUM_ACCOUNTS; ++a) { + auto acct = chain::name(static_cast(a + 1) * 0x10000000000000ULL); + // Lookup at the exact seq of the last version for this account + int v = VERSIONS_PER_ACCOUNT - 1; + uint64_t seq = static_cast(a * 100 + v * 10 + 1); + auto result = r.lookup(acct, seq); + BOOST_REQUIRE_MESSAGE(result.has_value(), "missing entry for account " << a << " version " << v); + auto expected = make_abi("a" + std::to_string(a) + "v" + std::to_string(v)); + BOOST_CHECK_EQUAL_COLLECTIONS(result->begin(), result->end(), expected.begin(), expected.end()); + } +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/plugins/trace_api_plugin/test/test_data_handlers.cpp b/plugins/trace_api_plugin/test/test_data_handlers.cpp index 32aa14a172..eefe1b78bf 100644 --- a/plugins/trace_api_plugin/test/test_data_handlers.cpp +++ b/plugins/trace_api_plugin/test/test_data_handlers.cpp @@ -3,11 +3,27 @@ #include #include +#include using namespace sysio; using namespace sysio::trace_api; using namespace sysio::trace_api::test_common; +namespace { + // Pack an abi_def into raw bytes that abi_data_handler can unpack + std::vector pack_abi(const chain::abi_def& abi) { + return fc::raw::pack(abi); + } + + // Build a lookup_fn that returns packed ABI bytes for a given account + abi_data_handler::abi_lookup_fn make_lookup(chain::name account, std::vector abi_bytes) { + return [account, bytes = std::move(abi_bytes)](chain::name a, uint64_t) -> std::optional> { + if (a == account) return bytes; + return std::nullopt; + }; + } +} + BOOST_AUTO_TEST_SUITE(abi_data_handler_tests) BOOST_AUTO_TEST_CASE(empty_data) { @@ -76,8 +92,7 @@ BOOST_AUTO_TEST_SUITE(abi_data_handler_tests) ); abi.version = "sysio::abi/1."; - abi_data_handler handler(exception_handler{}); - handler.add_abi("alice"_n, std::move(abi)); + abi_data_handler handler(exception_handler{}, make_lookup("alice"_n, pack_abi(abi))); fc::variant expected = fc::mutable_variant_object() ("a", 0) @@ -112,8 +127,7 @@ BOOST_AUTO_TEST_SUITE(abi_data_handler_tests) abi.version = "sysio::abi/1."; abi.action_results = { std::vector{ chain::action_result_def{ "foo"_n, "foor"} } }; - abi_data_handler handler(exception_handler{}); - handler.add_abi("alice"_n, std::move(abi)); + abi_data_handler handler(exception_handler{}, make_lookup("alice"_n, pack_abi(abi))); fc::variant expected = fc::mutable_variant_object() ("a", 0) @@ -151,8 +165,7 @@ BOOST_AUTO_TEST_SUITE(abi_data_handler_tests) ); abi.version = "sysio::abi/1."; - abi_data_handler handler(exception_handler{}); - handler.add_abi("alice"_n, std::move(abi)); + abi_data_handler handler(exception_handler{}, make_lookup("alice"_n, pack_abi(abi))); auto expected = fc::variant(); @@ -182,8 +195,10 @@ BOOST_AUTO_TEST_SUITE(abi_data_handler_tests) abi.version = "sysio::abi/1."; bool log_called = false; - abi_data_handler handler([&log_called](const exception_with_context& ){log_called = true;}); - handler.add_abi("alice"_n, std::move(abi)); + abi_data_handler handler( + [&log_called](const exception_with_context& ){log_called = true;}, + make_lookup("alice"_n, pack_abi(abi)) + ); auto expected = fc::variant(); @@ -214,8 +229,7 @@ BOOST_AUTO_TEST_SUITE(abi_data_handler_tests) ); abi.version = "sysio::abi/1."; - abi_data_handler handler(exception_handler{}); - handler.add_abi("alice"_n, std::move(abi)); + abi_data_handler handler(exception_handler{}, make_lookup("alice"_n, pack_abi(abi))); fc::variant expected = fc::mutable_variant_object() ("a", 0) diff --git a/plugins/trace_api_plugin/test/test_extraction.cpp b/plugins/trace_api_plugin/test/test_extraction.cpp index f1bf1a0ce8..f5ac8be515 100644 --- a/plugins/trace_api_plugin/test/test_extraction.cpp +++ b/plugins/trace_api_plugin/test/test_extraction.cpp @@ -113,6 +113,10 @@ struct extraction_test_fixture { return std::nullopt; // no prior data in unit tests } + void append_abi(chain::name, uint64_t, std::vector) { + // not tested here; abi_store tests cover this + } + extraction_test_fixture& fixture; }; diff --git a/tests/TestHarness/Cluster.py b/tests/TestHarness/Cluster.py index 591f99cc4c..9e86413d21 100644 --- a/tests/TestHarness/Cluster.py +++ b/tests/TestHarness/Cluster.py @@ -286,8 +286,6 @@ def launch(self, pnodes=1, unstartedNodes=0, totalNodes=1, prodCount=21, topo="m nodeopArgs += " --plugin sysio::producer_api_plugin" if prodsEnableTraceApi: nodeopArgs += " --plugin sysio::trace_api_plugin " - if extraNodeopArgs.find("--trace-rpc-abi") == -1: - nodeopArgs += " --trace-no-abis " httpMaxResponseTimeSet = False if specificExtraNodeopArgs is not None: assert(isinstance(specificExtraNodeopArgs, dict)) diff --git a/tests/TestHarness/launcher.py b/tests/TestHarness/launcher.py index cc87d348e4..ead7996328 100644 --- a/tests/TestHarness/launcher.py +++ b/tests/TestHarness/launcher.py @@ -565,8 +565,7 @@ def construct_command_line(self, instance: nodeDefinition): '--p2p-peer-address', '--p2p-auto-bp-peer', '--peer-key', '--peer-private-key', # producer_plugin '--producer-name', '--signature-provider', '--greylist-account', '--disable-subjective-account-billing', - # trace_api_plugin - '--trace-rpc-abi'] + ] for arg in specificList: if '-' in arg and arg not in repeatable: if arg in sysdcmd: diff --git a/tests/cli_test.py b/tests/cli_test.py index df7f1e4ac2..9bad41f55f 100755 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -353,7 +353,7 @@ def abi_file_with_nodeop_test(): os.makedirs(data_dir, exist_ok=True) walletMgr = WalletMgr(True) walletMgr.launch() - cmd = "./programs/nodeop/nodeop -e -p sysio --signature-provider wire-1,wire,wire,SYS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV,KEY:5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 --plugin sysio::trace_api_plugin --trace-no-abis --plugin sysio::producer_plugin --plugin sysio::producer_api_plugin --plugin sysio::chain_api_plugin --plugin sysio::chain_plugin --plugin sysio::http_plugin --access-control-allow-origin=* --http-validate-host=false --max-transaction-time=-1 --resource-monitor-not-shutdown-on-threshold-exceeded " + "--data-dir " + data_dir + " --config-dir " + data_dir + cmd = "./programs/nodeop/nodeop -e -p sysio --signature-provider wire-1,wire,wire,SYS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV,KEY:5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 --plugin sysio::trace_api_plugin --plugin sysio::producer_plugin --plugin sysio::producer_api_plugin --plugin sysio::chain_api_plugin --plugin sysio::chain_plugin --plugin sysio::http_plugin --access-control-allow-origin=* --http-validate-host=false --max-transaction-time=-1 --resource-monitor-not-shutdown-on-threshold-exceeded " + "--data-dir " + data_dir + " --config-dir " + data_dir node = Node('localhost', 8888, nodeId, data_dir=Path(data_dir), config_dir=Path(data_dir), cmd=shlex.split(cmd), launch_time=datetime.datetime.now().strftime('%Y_%m_%d_%H_%M_%S'), walletMgr=walletMgr) if not node or not Utils.waitForBool(node.checkPulse, timeout=15): Utils.Print("ERROR: node doesn't appear to be running...") diff --git a/tests/nodeop_lib_test.py b/tests/nodeop_lib_test.py index 772ad767d8..2fe3927419 100755 --- a/tests/nodeop_lib_test.py +++ b/tests/nodeop_lib_test.py @@ -62,8 +62,7 @@ if localTest and not dontLaunch: Print("Stand up cluster") - abs_path = os.path.abspath(os.getcwd() + '/contracts/sysio.token/sysio.token.abi') - traceNodeopArgs=" --http-max-response-time-ms 990000 --trace-rpc-abi sysio.token=" + abs_path + traceNodeopArgs=" --http-max-response-time-ms 990000" extraNodeopArgs=traceNodeopArgs + " --plugin sysio::prometheus_plugin --database-map-mode mapped_private " specificNodeopInstances={0: "bin/nodeop"} if cluster.launch(totalNodes=total_nodes, pnodes=pnodes, topo=topo, prodCount=prodCount, activateIF=activateIF, onlyBios=onlyBios, dontBootstrap=dontBootstrap, extraNodeopArgs=extraNodeopArgs, specificNodeopInstances=specificNodeopInstances) is False: diff --git a/tests/nodeop_run_test.py b/tests/nodeop_run_test.py index ea72345bcb..92d9daebbd 100755 --- a/tests/nodeop_run_test.py +++ b/tests/nodeop_run_test.py @@ -60,8 +60,7 @@ if localTest and not dontLaunch: Print("Stand up cluster") - abs_path = os.path.abspath(os.getcwd() + '/contracts/sysio.token/sysio.token.abi') - traceNodeopArgs=" --http-max-response-time-ms 990000 --trace-rpc-abi sysio.token=" + abs_path + traceNodeopArgs=" --http-max-response-time-ms 990000" extraNodeopArgs=traceNodeopArgs + " --plugin sysio::prometheus_plugin --database-map-mode mapped_private " specificNodeopInstances={0: "bin/nodeop"} if cluster.launch(totalNodes=2, prodCount=prodCount, activateIF=activateIF, onlyBios=onlyBios, dontBootstrap=dontBootstrap, extraNodeopArgs=extraNodeopArgs, specificNodeopInstances=specificNodeopInstances) is False: diff --git a/tests/plugin_http_api_test.py b/tests/plugin_http_api_test.py index fb4a5c92bb..6effb0aa9d 100755 --- a/tests/plugin_http_api_test.py +++ b/tests/plugin_http_api_test.py @@ -92,7 +92,7 @@ def startEnv(self) : "net_api_plugin", "producer_plugin", "producer_api_plugin", "chain_api_plugin", "http_plugin", "db_size_api_plugin", "prometheus_plugin"] nodeop_plugins = "--plugin sysio::" + " --plugin sysio::".join(plugin_names) - nodeop_flags = (" --data-dir=%s --config-dir=%s --trace-dir=%s --trace-no-abis --access-control-allow-origin=%s " + nodeop_flags = (" --data-dir=%s --config-dir=%s --trace-dir=%s --access-control-allow-origin=%s " "--signature-provider wire-1,wire,wire,SYS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV,KEY:5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3 " "--contracts-console --http-validate-host=%s --verbose-http-errors --max-transaction-time -1 --abi-serializer-max-time-ms 30000 --http-max-response-time-ms 30000 " "--p2p-peer-address localhost:9011 --resource-monitor-not-shutdown-on-threshold-exceeded ") % (self.data_dir, self.config_dir, self.data_dir, "\'*\'", "false") diff --git a/tests/resource_monitor_plugin_test.py b/tests/resource_monitor_plugin_test.py index ff641b3a0c..97e70b7719 100755 --- a/tests/resource_monitor_plugin_test.py +++ b/tests/resource_monitor_plugin_test.py @@ -178,9 +178,9 @@ def testAll(): testCommon("Resmon not enabled: no arguments", "", ["interval set to 2", "threshold set to 90", "Shutdown flag when threshold exceeded set to true", "snapshots's file system to be monitored", "blocks's file system to be monitored", "state's file system to be monitored"]) # default arguments with registered directories - testCommon("Resmon not enabled: Producer, Chain, State History and Trace Api", "--plugin sysio::state_history_plugin --state-history-dir=/tmp/state-history --disable-replay-opts --plugin sysio::trace_api_plugin --trace-dir=/tmp/trace --trace-no-abis", ["interval set to 2", "threshold set to 90", "Shutdown flag when threshold exceeded set to true", "snapshots's file system to be monitored", "blocks's file system to be monitored", "state's file system to be monitored", "state-history's file system to be monitored", "trace's file system to be monitored"]) + testCommon("Resmon not enabled: Producer, Chain, State History and Trace Api", "--plugin sysio::state_history_plugin --state-history-dir=/tmp/state-history --disable-replay-opts --plugin sysio::trace_api_plugin --trace-dir=/tmp/trace", ["interval set to 2", "threshold set to 90", "Shutdown flag when threshold exceeded set to true", "snapshots's file system to be monitored", "blocks's file system to be monitored", "state's file system to be monitored", "state-history's file system to be monitored", "trace's file system to be monitored"]) - testCommon("Resmon enabled: Producer, Chain, State History and Trace Api", "--plugin sysio::resource_monitor_plugin --plugin sysio::state_history_plugin --state-history-dir=/tmp/state-history --disable-replay-opts --plugin sysio::trace_api_plugin --trace-dir=/tmp/trace --trace-no-abis --resource-monitor-space-threshold=80 --resource-monitor-interval-seconds=3", ["snapshots's file system to be monitored", "blocks's file system to be monitored", "state's file system to be monitored", "state-history's file system to be monitored", "trace's file system to be monitored", "threshold set to 80", "interval set to 3", "Shutdown flag when threshold exceeded set to true"]) + testCommon("Resmon enabled: Producer, Chain, State History and Trace Api", "--plugin sysio::resource_monitor_plugin --plugin sysio::state_history_plugin --state-history-dir=/tmp/state-history --disable-replay-opts --plugin sysio::trace_api_plugin --trace-dir=/tmp/trace --resource-monitor-space-threshold=80 --resource-monitor-interval-seconds=3", ["snapshots's file system to be monitored", "blocks's file system to be monitored", "state's file system to be monitored", "state-history's file system to be monitored", "trace's file system to be monitored", "threshold set to 80", "interval set to 3", "Shutdown flag when threshold exceeded set to true"]) # Only test minimum warning threshold (i.e. 6) to trigger warning as much as possible testInterval("Resmon enabled: set warning interval", diff --git a/tests/trace_plugin_test.py b/tests/trace_plugin_test.py index 63ad70854a..d9ec626bbd 100755 --- a/tests/trace_plugin_test.py +++ b/tests/trace_plugin_test.py @@ -20,8 +20,7 @@ class TraceApiPluginTest(unittest.TestCase): # start kiod and nodeop def startEnv(self) : account_names = ["alice", "bob", "charlie"] - abs_path = os.path.abspath(os.getcwd() + '/contracts/sysio.token/sysio.token.abi') - extraNodeopArgs = " --verbose-http-errors --trace-slice-stride 10000 --trace-rpc-abi sysio.token=" + abs_path + extraNodeopArgs = " --verbose-http-errors --trace-slice-stride 10000" extraNodeopArgs+=" --production-pause-vote-timeout-ms 0" self.cluster.launch(totalNodes=2, activateIF=True, extraNodeopArgs=extraNodeopArgs) self.walletMgr.launch() diff --git a/tools/cluster_manager.py b/tools/cluster_manager.py index b453e69e47..6ccef013af 100755 --- a/tools/cluster_manager.py +++ b/tools/cluster_manager.py @@ -254,7 +254,6 @@ def _create_cluster( " --contracts-console" " --plugin sysio::producer_api_plugin" " --plugin sysio::trace_api_plugin" - " --trace-no-abis" " --http-max-response-time-ms 990000" ) args_arr.extend(["--nodeop", nodeop_args]) diff --git a/tutorials/bios-boot-tutorial/bios-boot-tutorial.py b/tutorials/bios-boot-tutorial/bios-boot-tutorial.py index 996eb1e32a..95128b4c85 100755 --- a/tutorials/bios-boot-tutorial/bios-boot-tutorial.py +++ b/tutorials/bios-boot-tutorial/bios-boot-tutorial.py @@ -94,7 +94,7 @@ def startNode(nodeIndex, account): run('mkdir -p ' + dir) otherOpts = ''.join(list(map(lambda i: ' --p2p-peer-address localhost:' + str(9000 + i), range(nodeIndex)))) if not nodeIndex: otherOpts += ( - ' --plugin sysio::trace_api_plugin --trace-no-abis' + ' --plugin sysio::trace_api_plugin' ) # if SVN blsFinKeys cmd = ( From b797a5be2576de6ff07fa55c41d2ad3cd933c0a2 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Mon, 13 Apr 2026 15:56:34 -0500 Subject: [PATCH 04/32] trace_api: add POST /v1/trace_api/get_actions and get_token_transfers endpoints get_actions: paginated action search over a block range with optional filters on receiver, account, and action name. Pagination uses an after_global_seq cursor and returns more+last_global_seq for the next page. ABI-decoded params are included when available. get_token_transfers: convenience preset of get_actions with receiver=account=token_contract (default sysio.token), action=transfer. Using receiver=token_contract yields exactly one result per transfer -- the canonical execution, not the inline notification copies. Also fixes a latent UB: fc::to_hex is now guarded against empty data vectors (nullptr .data()). Adds 12 unit tests covering filters, pagination, multi-block scan, ABI decoding, and the token transfer deduplication behaviour. --- .../sysio/trace_api/request_handler.hpp | 102 +++++ .../trace_api_plugin/src/trace_api_plugin.cpp | 103 +++++ plugins/trace_api_plugin/test/CMakeLists.txt | 1 + .../test/test_get_actions.cpp | 374 ++++++++++++++++++ 4 files changed, 580 insertions(+) create mode 100644 plugins/trace_api_plugin/test/test_get_actions.cpp diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp index 4afae4d35a..31842e40c3 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp @@ -1,6 +1,10 @@ #pragma once +#include +#include #include +#include +#include #include #include #include @@ -15,6 +19,28 @@ namespace sysio::trace_api { }; } + /** + * Filter parameters for the get_actions endpoint. + */ + struct action_query { + std::optional receiver; ///< filter by receiver account (any if unset) + std::optional account; ///< filter by contract/code account (any if unset) + std::optional action; ///< filter by action name (any if unset) + uint32_t block_num_start = 0; + uint32_t block_num_end = std::numeric_limits::max(); + uint64_t after_global_seq = 0; ///< pagination cursor: skip actions with global_seq <= this + uint32_t limit = 100; ///< max results per request (clamped to 1000 by the handler) + }; + + /** + * Result returned by get_actions. + */ + struct actions_result { + fc::variants actions; + bool more = false; ///< true if there are more results past 'limit' + uint64_t last_global_seq = 0; ///< global_seq of the last returned action; use as after_global_seq for the next page + }; + template class request_handler { public: @@ -89,6 +115,82 @@ namespace sysio::trace_api { return result; } + /** + * Scan a block range for action traces matching the given filter and return paginated results. + * + * Blocks are scanned in ascending order. Within each block, actions are visited in ascending + * global_sequence order. Scanning stops as soon as 'limit' matching actions are found, and + * 'more' is set to true to signal that the caller should paginate. + * + * Use the returned 'last_global_seq' as 'after_global_seq' on the next call to continue from + * where this call left off. + * + * @param query - filter and pagination parameters + * @return actions_result containing the matching actions and pagination state + */ + actions_result get_actions(const action_query& query) { + actions_result result; + + const uint32_t end = query.block_num_end; + for (uint32_t block_num = query.block_num_start; block_num <= end; ++block_num) { + auto data = logfile_provider.get_block(block_num); + if (!data) continue; + + std::visit([&](const auto& bt) { + for (const auto& trx : bt.transactions) { + // actions within a transaction are already in global_sequence order after + // process_actions sorts them, but the raw vector may not be — sort here + std::vector sorted; + sorted.reserve(trx.actions.size()); + for (const auto& a : trx.actions) + sorted.push_back(&a); + std::sort(sorted.begin(), sorted.end(), [](const auto* l, const auto* r){ + return l->global_sequence < r->global_sequence; + }); + + for (const action_trace_v0* ap : sorted) { + const auto& a = *ap; + if (a.global_sequence <= query.after_global_seq) continue; + if (query.receiver && a.receiver != *query.receiver) continue; + if (query.account && a.account != *query.account) continue; + if (query.action && a.action != *query.action) continue; + + auto action_var = fc::mutable_variant_object() + ("global_sequence", a.global_sequence) + ("receiver", a.receiver.to_string()) + ("account", a.account.to_string()) + ("action", a.action.to_string()) + ("data", a.data.empty() ? "" : fc::to_hex(a.data.data(), a.data.size())) + ("return_value", a.return_value.empty() ? "" : fc::to_hex(a.return_value.data(), a.return_value.size())) + ("trx_id", trx.id.str()) + ("block_num", trx.block_num) + ("block_time", trx.block_time) + ("producer_block_id", trx.producer_block_id); + + auto [params, return_data] = data_handler_provider.serialize_to_variant(a); + if (!params.is_null()) + action_var("params", params); + if (return_data.has_value()) + action_var("return_data", *return_data); + + result.last_global_seq = a.global_sequence; + result.actions.push_back(std::move(action_var)); + + if (result.actions.size() >= query.limit) { + result.more = true; + return; // exits the lambda; outer loop will also check result.more + } + } + if (result.more) return; + } + }, std::get<0>(*data)); + + if (result.more) break; + } + + return result; + } + private: LogfileProvider logfile_provider; DataHandlerProvider data_handler_provider; diff --git a/plugins/trace_api_plugin/src/trace_api_plugin.cpp b/plugins/trace_api_plugin/src/trace_api_plugin.cpp index 9a8123fc38..ea8a967cd6 100644 --- a/plugins/trace_api_plugin/src/trace_api_plugin.cpp +++ b/plugins/trace_api_plugin/src/trace_api_plugin.cpp @@ -312,6 +312,109 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_this(); + if (obj.contains("block_num_end")) + query.block_num_end = obj["block_num_end"].as(); + if (obj.contains("after_global_seq")) + query.after_global_seq = obj["after_global_seq"].as_uint64(); + if (obj.contains("limit")) + query.limit = std::min(obj["limit"].as(), 1000u); + } catch (...) { + error_results results{400, "Bad request body"}; + cb( 400, fc::variant( results )); + return; + } + } + + if (query.block_num_start > query.block_num_end) { + error_results results{400, "block_num_start must be <= block_num_end"}; + cb( 400, fc::variant( results )); + return; + } + + try { + auto result = req_handler->get_actions(query); + cb( 200, fc::mutable_variant_object() + ("actions", result.actions) + ("more", result.more) + ("last_global_seq", result.last_global_seq) + ); + } catch (...) { + http_plugin::handle_exception("trace_api", "get_actions", body, cb); + } + }}); + + http.add_async_handler({"/v1/trace_api/get_token_transfers", + api_category::trace_api, + [this](std::string, std::string body, url_response_callback cb) + { + // Convenience wrapper: receiver=account=token_contract, action=transfer. + // Using receiver=token_contract ensures exactly one result per transfer + // (no duplicate inline-notification entries for the recipient). + action_query query; + query.action = chain::name("transfer"); + + if (!body.empty()) { + try { + auto input = fc::json::from_string(body); + const auto& obj = input.get_object(); + chain::name token_contract("sysio.token"); + if (obj.contains("token_contract")) + token_contract = chain::name(obj["token_contract"].as_string()); + query.receiver = token_contract; + query.account = token_contract; + if (obj.contains("block_num_start")) + query.block_num_start = obj["block_num_start"].as(); + if (obj.contains("block_num_end")) + query.block_num_end = obj["block_num_end"].as(); + if (obj.contains("after_global_seq")) + query.after_global_seq = obj["after_global_seq"].as_uint64(); + if (obj.contains("limit")) + query.limit = std::min(obj["limit"].as(), 1000u); + } catch (...) { + error_results results{400, "Bad request body"}; + cb( 400, fc::variant( results )); + return; + } + } else { + query.receiver = chain::name("sysio.token"); + query.account = chain::name("sysio.token"); + } + + if (query.block_num_start > query.block_num_end) { + error_results results{400, "block_num_start must be <= block_num_end"}; + cb( 400, fc::variant( results )); + return; + } + + try { + auto result = req_handler->get_actions(query); + cb( 200, fc::mutable_variant_object() + ("transfers", result.actions) + ("more", result.more) + ("last_global_seq", result.last_global_seq) + ); + } catch (...) { + http_plugin::handle_exception("trace_api", "get_token_transfers", body, cb); + } + }}); } void plugin_shutdown() { diff --git a/plugins/trace_api_plugin/test/CMakeLists.txt b/plugins/trace_api_plugin/test/CMakeLists.txt index ff8286b717..f11e5a046b 100644 --- a/plugins/trace_api_plugin/test/CMakeLists.txt +++ b/plugins/trace_api_plugin/test/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable( test_trace_api_plugin test_compressed_file.cpp test_trx_id_index.cpp test_abi_store.cpp + test_get_actions.cpp main.cpp ) target_link_libraries( test_trace_api_plugin trace_api_plugin ) diff --git a/plugins/trace_api_plugin/test/test_get_actions.cpp b/plugins/trace_api_plugin/test/test_get_actions.cpp new file mode 100644 index 0000000000..e64689d862 --- /dev/null +++ b/plugins/trace_api_plugin/test/test_get_actions.cpp @@ -0,0 +1,374 @@ +#include + +#include +#include + +using namespace sysio; +using namespace sysio::trace_api; +using namespace sysio::trace_api::test_common; + +namespace { + +// --------------------------------------------------------------------------- +// Fixture +// --------------------------------------------------------------------------- + +struct get_actions_fixture { + struct mock_logfile_provider { + mock_logfile_provider(get_actions_fixture& f) : fixture(f) {} + + get_block_t get_block(uint32_t height) { + auto it = fixture.blocks.find(height); + if (it == fixture.blocks.end()) return {}; + return std::make_tuple(data_log_entry{it->second}, true /*irreversible*/); + } + + get_actions_fixture& fixture; + }; + + struct mock_data_handler_provider { + mock_data_handler_provider(get_actions_fixture& f) : fixture(f) {} + + std::tuple> serialize_to_variant(const action_trace_v0& a) { + return fixture.mock_data_handler(a); + } + + get_actions_fixture& fixture; + }; + + using impl_type = request_handler; + + get_actions_fixture() + : impl(mock_logfile_provider(*this), mock_data_handler_provider(*this), + [](const std::string& msg){ fc_dlog(fc::logger::default_logger(), "{}", msg); }) + {} + + actions_result get_actions(const action_query& query) { + return impl.get_actions(query); + } + + // Default: no ABI decoding — params/return_data absent from result + std::function>(const action_trace_v0&)> + mock_data_handler = [](const action_trace_v0&) -> std::tuple> { + return {}; + }; + + std::map blocks; + impl_type impl; +}; + +// --------------------------------------------------------------------------- +// Builders +// --------------------------------------------------------------------------- + +action_trace_v0 make_action(uint64_t seq, chain::name receiver, chain::name account, + chain::name act, chain::bytes data = {}) { + return { seq, receiver, account, act, {}, std::move(data), {} }; +} + +transaction_trace_v0 make_trx(chain::transaction_id_type id, uint32_t block_num, + std::vector actions) { + transaction_trace_v0 trx; + trx.id = id; + trx.actions = std::move(actions); + trx.status = fc::enum_type{ + chain::transaction_receipt_header::status_enum::executed}; + trx.block_num = block_num; + trx.block_time = chain::block_timestamp_type(0); + return trx; +} + +block_trace_v0 make_block(uint32_t num, std::vector trxs) { + block_trace_v0 blk; + blk.number = num; + blk.producer = "bp.one"_n; + blk.transactions = std::move(trxs); + return blk; +} + +// Convenience: one action per block, sequential global_sequences +static const chain::transaction_id_type TRX1 = + "0000000000000000000000000000000000000000000000000000000000000001"_h; +static const chain::transaction_id_type TRX2 = + "0000000000000000000000000000000000000000000000000000000000000002"_h; +static const chain::transaction_id_type TRX3 = + "0000000000000000000000000000000000000000000000000000000000000003"_h; + +} // anonymous namespace + +// --------------------------------------------------------------------------- +// Test suite +// --------------------------------------------------------------------------- + +BOOST_AUTO_TEST_SUITE(get_actions_tests) + +// No blocks in the queried range -> empty result +BOOST_FIXTURE_TEST_CASE(empty_range, get_actions_fixture) +{ + action_query q; + q.block_num_start = 1; + q.block_num_end = 10; + + auto r = get_actions(q); + + BOOST_TEST(r.actions.empty()); + BOOST_TEST(!r.more); + BOOST_TEST(r.last_global_seq == 0u); +} + +// Filter that matches nothing returns empty result +BOOST_FIXTURE_TEST_CASE(no_matching_filter, get_actions_fixture) +{ + blocks[1] = make_block(1, { + make_trx(TRX1, 1, { make_action(1, "sysio.token"_n, "sysio.token"_n, "transfer"_n) }) + }); + + action_query q; + q.block_num_start = 1; + q.block_num_end = 1; + q.receiver = "alice"_n; // alice is not a receiver in this block + + auto r = get_actions(q); + + BOOST_TEST(r.actions.empty()); + BOOST_TEST(!r.more); +} + +// receiver filter: keeps only actions where receiver matches +BOOST_FIXTURE_TEST_CASE(filter_by_receiver, get_actions_fixture) +{ + // Two actions: original transfer (receiver=sysio.token) + inline notification (receiver=bob) + blocks[1] = make_block(1, { + make_trx(TRX1, 1, { + make_action(1, "sysio.token"_n, "sysio.token"_n, "transfer"_n), + make_action(2, "bob"_n, "sysio.token"_n, "transfer"_n) + }) + }); + + action_query q; + q.block_num_start = 1; + q.block_num_end = 1; + q.receiver = "sysio.token"_n; + + auto r = get_actions(q); + + BOOST_REQUIRE_EQUAL(r.actions.size(), 1u); + BOOST_TEST(r.actions[0].get_object()["receiver"].as_string() == "sysio.token"); + BOOST_TEST(r.actions[0].get_object()["global_sequence"].as_uint64() == 1u); + BOOST_TEST(!r.more); + BOOST_TEST(r.last_global_seq == 1u); +} + +// account filter: keeps only actions where account (code) matches +BOOST_FIXTURE_TEST_CASE(filter_by_account, get_actions_fixture) +{ + blocks[1] = make_block(1, { + make_trx(TRX1, 1, { + make_action(1, "alice"_n, "sysio.token"_n, "transfer"_n), + make_action(2, "alice"_n, "mycontract"_n, "foo"_n) + }) + }); + + action_query q; + q.block_num_start = 1; + q.block_num_end = 1; + q.account = "sysio.token"_n; + + auto r = get_actions(q); + + BOOST_REQUIRE_EQUAL(r.actions.size(), 1u); + BOOST_TEST(r.actions[0].get_object()["account"].as_string() == "sysio.token"); + BOOST_TEST(r.actions[0].get_object()["global_sequence"].as_uint64() == 1u); +} + +// action name filter: keeps only the named action +BOOST_FIXTURE_TEST_CASE(filter_by_action_name, get_actions_fixture) +{ + blocks[1] = make_block(1, { + make_trx(TRX1, 1, { + make_action(1, "sysio.token"_n, "sysio.token"_n, "transfer"_n), + make_action(2, "sysio"_n, "sysio"_n, "newaccount"_n) + }) + }); + + action_query q; + q.block_num_start = 1; + q.block_num_end = 1; + q.action = "transfer"_n; + + auto r = get_actions(q); + + BOOST_REQUIRE_EQUAL(r.actions.size(), 1u); + BOOST_TEST(r.actions[0].get_object()["action"].as_string() == "transfer"); +} + +// limit caps the number of returned results; more=true when there are additional results +BOOST_FIXTURE_TEST_CASE(pagination_limit, get_actions_fixture) +{ + blocks[1] = make_block(1, { + make_trx(TRX1, 1, { + make_action(1, "a"_n, "tok"_n, "transfer"_n), + make_action(2, "a"_n, "tok"_n, "transfer"_n), + make_action(3, "a"_n, "tok"_n, "transfer"_n), + make_action(4, "a"_n, "tok"_n, "transfer"_n), + make_action(5, "a"_n, "tok"_n, "transfer"_n) + }) + }); + + action_query q; + q.block_num_start = 1; + q.block_num_end = 1; + q.limit = 3; + + auto r = get_actions(q); + + BOOST_REQUIRE_EQUAL(r.actions.size(), 3u); + BOOST_TEST(r.more); + BOOST_TEST(r.last_global_seq == 3u); +} + +// after_global_seq skips already-seen actions for cursor-based pagination +BOOST_FIXTURE_TEST_CASE(pagination_after_global_seq, get_actions_fixture) +{ + blocks[1] = make_block(1, { + make_trx(TRX1, 1, { + make_action(1, "a"_n, "tok"_n, "transfer"_n), + make_action(2, "a"_n, "tok"_n, "transfer"_n), + make_action(3, "a"_n, "tok"_n, "transfer"_n), + make_action(4, "a"_n, "tok"_n, "transfer"_n), + make_action(5, "a"_n, "tok"_n, "transfer"_n) + }) + }); + + action_query q; + q.block_num_start = 1; + q.block_num_end = 1; + q.after_global_seq = 3; // skip seq 1-3 + + auto r = get_actions(q); + + BOOST_REQUIRE_EQUAL(r.actions.size(), 2u); + BOOST_TEST(r.actions[0].get_object()["global_sequence"].as_uint64() == 4u); + BOOST_TEST(r.actions[1].get_object()["global_sequence"].as_uint64() == 5u); + BOOST_TEST(!r.more); + BOOST_TEST(r.last_global_seq == 5u); +} + +// Actions are returned across multiple blocks; missing blocks in the log are skipped +BOOST_FIXTURE_TEST_CASE(multi_block_scan, get_actions_fixture) +{ + blocks[1] = make_block(1, { make_trx(TRX1, 1, { make_action(1, "a"_n, "tok"_n, "transfer"_n) }) }); + blocks[2] = make_block(2, { make_trx(TRX2, 2, { make_action(2, "a"_n, "tok"_n, "transfer"_n) }) }); + // block 3 is missing (gap in trace log) + blocks[4] = make_block(4, { make_trx(TRX3, 4, { make_action(3, "a"_n, "tok"_n, "transfer"_n) }) }); + + action_query q; + q.block_num_start = 1; + q.block_num_end = 5; + + auto r = get_actions(q); + + BOOST_REQUIRE_EQUAL(r.actions.size(), 3u); + BOOST_TEST(r.actions[0].get_object()["block_num"].as() == 1u); + BOOST_TEST(r.actions[1].get_object()["block_num"].as() == 2u); + BOOST_TEST(r.actions[2].get_object()["block_num"].as() == 4u); + BOOST_TEST(!r.more); + BOOST_TEST(r.last_global_seq == 3u); +} + +// ABI-decoded params are included in the result when the data handler returns them +BOOST_FIXTURE_TEST_CASE(abi_decoded_params_included, get_actions_fixture) +{ + blocks[1] = make_block(1, { + make_trx(TRX1, 1, { + make_action(1, "sysio.token"_n, "sysio.token"_n, "transfer"_n, {0x01, 0x02}) + }) + }); + + mock_data_handler = [](const action_trace_v0&) -> std::tuple> { + return { fc::mutable_variant_object()("amount", 100), std::nullopt }; + }; + + action_query q; + q.block_num_start = 1; + q.block_num_end = 1; + + auto r = get_actions(q); + + BOOST_REQUIRE_EQUAL(r.actions.size(), 1u); + const auto& obj = r.actions[0].get_object(); + BOOST_REQUIRE(obj.contains("params")); + BOOST_TEST(obj["params"].get_object()["amount"].as() == 100); + BOOST_TEST(!obj.contains("return_data")); +} + +// When the data handler returns null, no params field is emitted +BOOST_FIXTURE_TEST_CASE(no_params_when_handler_returns_null, get_actions_fixture) +{ + blocks[1] = make_block(1, { + make_trx(TRX1, 1, { make_action(1, "alice"_n, "contract"_n, "foo"_n, {static_cast(0xAB)}) }) + }); + + action_query q; + q.block_num_start = 1; + q.block_num_end = 1; + + auto r = get_actions(q); + + BOOST_REQUIRE_EQUAL(r.actions.size(), 1u); + const auto& obj = r.actions[0].get_object(); + BOOST_TEST(!obj.contains("params")); + BOOST_TEST(obj["data"].as_string() == "ab"); // raw hex always present +} + +// Verifies the receiver+account+action filter used by get_token_transfers: +// exactly one result per transfer (the original; notification copy is excluded) +BOOST_FIXTURE_TEST_CASE(token_transfer_filter_excludes_notifications, get_actions_fixture) +{ + blocks[1] = make_block(1, { + make_trx(TRX1, 1, { + make_action(1, "sysio.token"_n, "sysio.token"_n, "transfer"_n), // original + make_action(2, "bob"_n, "sysio.token"_n, "transfer"_n) // inline notification + }) + }); + + // This is the filter preset used by POST /v1/trace_api/get_token_transfers + action_query q; + q.block_num_start = 1; + q.block_num_end = 1; + q.receiver = "sysio.token"_n; + q.account = "sysio.token"_n; + q.action = "transfer"_n; + + auto r = get_actions(q); + + BOOST_REQUIRE_EQUAL(r.actions.size(), 1u); + BOOST_TEST(r.actions[0].get_object()["global_sequence"].as_uint64() == 1u); + BOOST_TEST(r.actions[0].get_object()["receiver"].as_string() == "sysio.token"); +} + +// Actions within a transaction are visited in ascending global_sequence order +// regardless of their storage order in the vector +BOOST_FIXTURE_TEST_CASE(actions_sorted_by_global_sequence, get_actions_fixture) +{ + blocks[1] = make_block(1, { + make_trx(TRX1, 1, { + make_action(5, "a"_n, "tok"_n, "transfer"_n), // out of order + make_action(1, "a"_n, "tok"_n, "transfer"_n), + make_action(3, "a"_n, "tok"_n, "transfer"_n) + }) + }); + + action_query q; + q.block_num_start = 1; + q.block_num_end = 1; + + auto r = get_actions(q); + + BOOST_REQUIRE_EQUAL(r.actions.size(), 3u); + BOOST_TEST(r.actions[0].get_object()["global_sequence"].as_uint64() == 1u); + BOOST_TEST(r.actions[1].get_object()["global_sequence"].as_uint64() == 3u); + BOOST_TEST(r.actions[2].get_object()["global_sequence"].as_uint64() == 5u); +} + +BOOST_AUTO_TEST_SUITE_END() From 58df0fff7d5ed78d8ea4728eb99afaad64989011 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Mon, 13 Apr 2026 17:18:55 -0500 Subject: [PATCH 05/32] trace_api: add --trace-max-query-limit option, docs, and integration test assertions - Add --trace-max-query-limit (default 1000, -1=unlimited) so operators running private nodes can remove per-request caps on get_actions / get_token_transfers queries - Add comprehensive plugin documentation covering all endpoints, configuration options, on-disk layout, ABI decoding, pagination, and exchange/indexer integration guidance - Extend nodeop_run_test.py with get_actions and get_token_transfers assertions (params decoded, trx_id matching, receiver filtering) - Fix Cluster.py: add trailing space after producer_api_plugin arg to prevent concatenation with subsequent extra args when spacer was removed --- .../trace_api_plugin/src/trace_api_plugin.cpp | 16 +- plugins/trace_api_plugin/trace_api_plugin.md | 661 ++++++++++++++++++ tests/TestHarness/Cluster.py | 2 +- tests/nodeop_run_test.py | 39 ++ 4 files changed, 715 insertions(+), 3 deletions(-) create mode 100644 plugins/trace_api_plugin/trace_api_plugin.md diff --git a/plugins/trace_api_plugin/src/trace_api_plugin.cpp b/plugins/trace_api_plugin/src/trace_api_plugin.cpp index ea8a967cd6..d21f7b13b8 100644 --- a/plugins/trace_api_plugin/src/trace_api_plugin.cpp +++ b/plugins/trace_api_plugin/src/trace_api_plugin.cpp @@ -129,6 +129,9 @@ struct trace_api_common_impl { cfg_options("trace-minimum-uncompressed-irreversible-history-blocks", boost::program_options::value()->default_value(-1), "Number of blocks to ensure are uncompressed past LIB. Compressed \"slice\" files are still accessible but may carry a performance loss on retrieval\n" "A value of -1 indicates that automatic compression of \"slice\" files will be turned off."); + cfg_options("trace-max-query-limit", bpo::value()->default_value(1000), + "Maximum number of results returned by a single get_actions or get_token_transfers request.\n" + "A value of -1 removes the limit (use with caution on public nodes)."); } void plugin_initialize(const appbase::variables_map& options) { @@ -157,6 +160,12 @@ struct trace_api_common_impl { minimum_uncompressed_irreversible_history_blocks = uncompressed_blocks; } + const int32_t query_limit = options.at("trace-max-query-limit").as(); + SYS_ASSERT(query_limit >= -1, chain::plugin_config_exception, + "\"trace-max-query-limit\" must be -1 (unlimited) or a positive value."); + max_query_limit = (query_limit == -1) ? std::numeric_limits::max() + : static_cast(query_limit); + store = std::make_shared( trace_dir, slice_stride, @@ -186,6 +195,7 @@ struct trace_api_common_impl { static constexpr int32_t manual_slice_file_value = -1; static constexpr uint32_t compression_seek_point_stride = 6 * 1024 * 1024; // 6 MiB strides for clog seek points + uint32_t max_query_limit = 1000; std::shared_ptr store; }; @@ -201,6 +211,7 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_thismax_query_limit; auto store = common->store; auto data_handler = std::make_shared( [](const exception_with_context& e) { @@ -335,7 +346,7 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_this(), 1000u); + query.limit = std::min(obj["limit"].as(), max_query_limit); } catch (...) { error_results results{400, "Bad request body"}; cb( 400, fc::variant( results )); @@ -387,7 +398,7 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_this(), 1000u); + query.limit = std::min(obj["limit"].as(), max_query_limit); } catch (...) { error_results results{400, "Bad request body"}; cb( 400, fc::variant( results )); @@ -421,6 +432,7 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_this common; + uint32_t max_query_limit = 1000; using request_handler_t = request_handler, abi_data_handler::shared_provider>; std::shared_ptr req_handler; diff --git a/plugins/trace_api_plugin/trace_api_plugin.md b/plugins/trace_api_plugin/trace_api_plugin.md new file mode 100644 index 0000000000..2323045ed7 --- /dev/null +++ b/plugins/trace_api_plugin/trace_api_plugin.md @@ -0,0 +1,661 @@ +# trace_api_plugin + +Full-history action trace plugin for Wire nodeop. Captures every action +trace as blocks are applied, persists them in a structured on-disk store, +and exposes HTTP endpoints for querying traces, transactions, actions, and +token transfers. + +--- + +## Table of contents + +1. [Overview](#overview) +2. [Enabling the plugin](#enabling-the-plugin) +3. [Configuration options](#configuration-options) +4. [On-disk layout](#on-disk-layout) + - [Slice files](#slice-files) + - [Transaction-id index](#transaction-id-index) + - [ABI store](#abi-store) +5. [Startup continuity check](#startup-continuity-check) +6. [ABI decoding](#abi-decoding) +7. [HTTP API reference](#http-api-reference) + - [get_block](#get_block) + - [get_transaction_trace](#get_transaction_trace) + - [get_actions](#get_actions) + - [get_token_transfers](#get_token_transfers) +8. [Pagination guide](#pagination-guide) +9. [Exchange / indexer integration guide](#exchange--indexer-integration-guide) +10. [Maintenance and retention](#maintenance-and-retention) +11. [Plugin variants](#plugin-variants) + +--- + +## Overview + +The trace_api_plugin writes a complete record of every action executed on +chain (including inline actions), alongside the ABI in effect at the time +each contract was called. That data is kept on disk indefinitely (or for a +configurable retention window) and is served through a set of HTTP endpoints +without touching the chainbase database. + +Key design points: + +- **No chainbase dependency at query time** — responses are built entirely + from the trace files on disk. +- **Inline actions included** — every entry in `chain::transaction_trace:: + action_traces` is stored, so inline notifications are captured alongside + the originating action. +- **Versioned ABI decoding** — the ABI in effect at the moment each + `setcode`/`setabi` transaction was applied is captured in `abi_store.log`. + Queries decode `data` and `return_value` fields using the historically + correct ABI, not the current on-chain ABI. +- **O(1) transaction lookup** — a per-slice hash index maps `trx_id` to + `block_num` so `get_transaction_trace` does not scan the chain. + +--- + +## Enabling the plugin + +Add to `config.ini` or pass on the command line: + +```ini +plugin = sysio::trace_api_plugin +trace-dir = traces +``` + +Or via CLI: + +```bash +nodeop --plugin sysio::trace_api_plugin --trace-dir /var/lib/nodeop/traces +``` + +The plugin also requires `chain_plugin` and `http_plugin` (both loaded by +default). + +--- + +## Configuration options + +| Option | Default | Description | +|--------|---------|-------------| +| `trace-dir` | `traces` | Directory for trace files. Relative paths are resolved from the node's data directory. | +| `trace-slice-stride` | `10000` | Number of blocks per slice file. Larger values reduce file count but increase the amount of data re-scanned when a single slice is accessed. | +| `trace-minimum-irreversible-history-blocks` | `-1` | Blocks past LIB to retain before old slices can be auto-deleted. `-1` disables automatic deletion (keep forever). | +| `trace-minimum-uncompressed-irreversible-history-blocks` | `-1` | Blocks past LIB to keep uncompressed. Slices older than this threshold are transparently compressed. `-1` disables automatic compression. | +| `trace-max-query-limit` | `1000` | Maximum number of results a single `get_actions` or `get_token_transfers` request may return. Client-supplied `limit` values are clamped to this. Set to `-1` to remove the server-side cap entirely. | + +### Recommended production settings + +```ini +plugin = sysio::trace_api_plugin +trace-dir = /var/lib/nodeop/traces +trace-slice-stride = 10000 +# Keep 2 weeks of uncompressed data (~2M blocks/day = ~28M blocks) +trace-minimum-uncompressed-irreversible-history-blocks = 28000000 +# Keep 1 year total (compressed) +trace-minimum-irreversible-history-blocks = 365000000 +``` + +For a full-history archive node omit or set both retention options to `-1`. + +--- + +## On-disk layout + +All files live inside `trace-dir`. The directory is monitored by +`resource_monitor_plugin` when that plugin is loaded. + +### Slice files + +Blocks are grouped into contiguous slices of `trace-slice-stride` blocks +each. Each slice is represented by three files that share a common range +suffix `-` (zero-padded to 10 digits): + +| File | Description | +|------|-------------| +| `trace_-.log` | Serialized `block_trace_v0` records (action data). | +| `trace_index_-.log` | Metadata index: block number → byte offset in the trace file. Enables random access without scanning the full trace file. | +| `trace_trx_idx_-.log` | Transaction-id hash index (see below). | + +When a slice is compressed the trace file is replaced by: + +| File | Description | +|------|-------------| +| `trace_-.clog` | zlib-compressed trace data with embedded seek points for random access. | + +The index and trx_id index files are not compressed. + +**Example** (10 000-block stride, blocks 0–29 999): + +``` +traces/ + trace_0000000000-0000010000.log + trace_index_0000000000-0000010000.log + trace_trx_idx_0000000000-0000010000.log + trace_0000010000-0000020000.log + trace_index_0000010000-0000020000.log + trace_trx_idx_0000010000-0000020000.log + trace_0000020000-0000030000.clog <- compressed + trace_index_0000020000-0000030000.log + trace_trx_idx_0000020000-0000030000.log + abi_store.log +``` + +### Transaction-id index + +`trace_trx_idx_-.log` is a compact open-addressing hash table +(load factor ≤ 0.5, linear probing) that maps a 64-bit prefix of a +transaction SHA-256 to the block number containing that transaction. + +- **Header** (16 bytes): magic `TRIX`, version 1, bucket_count, reserved. +- **Buckets** (16 bytes each): `prefix64 (u64)` + `block_num (u32)` + + `reserved (u32)`. Empty slots have `block_num == 0`. + +The index is built once per slice when the slice's last block becomes +irreversible. Queries against `/v1/trace_api/get_transaction_trace` use +this index for O(1) `trx_id → block_num` resolution instead of scanning +the chain. + +### ABI store + +`abi_store.log` is a single file that persists the ABI published by each +contract account across all `setabi` transactions observed since the node +started (or since the file was first written). + +Format: + +``` +Header (16 bytes): magic "ABIB", version 1, entry_count, reserved +Index (entry_count × 24 bytes, sorted account ASC, global_seq ASC): + account(u64) | global_seq(u64) | blob_offset(u32) | blob_size(u32) +Blob area (variable): raw fc::raw-packed abi_def bytes in index order +``` + +To find the ABI for contract `A` in effect at `global_seq Q`: binary-search +the index for the last entry where `account == A && global_seq <= Q`, then +read the referenced blob. + +The file is written atomically (write to `.tmp`, then rename) after each +block. On node restart it is loaded into the writer so previously captured +ABIs survive across restarts. + +--- + +## Startup continuity check + +On plugin startup the trace store's recorded block range is compared against +the chain's current head. Three outcomes are logged: + +| Situation | Log level | Description | +|-----------|-----------|-------------| +| No prior trace data | `info` | Fresh start; tracing begins at the current head. | +| Snapshot restore detected | `warning` | The snapshot skips blocks already in the trace store, creating a gap. Trace data for the skipped range is inaccessible. | +| Normal restart | `info` | Store is contiguous with the chain head; tracing resumes normally. | + +A continuity gap does not prevent the node from running, but `get_block` and +`get_transaction_trace` requests for block numbers inside the gap will return +404. + +--- + +## ABI decoding + +When serving any trace endpoint the plugin attempts to decode the raw `data` +and `return_value` bytes of each action using the ABI captured in +`abi_store.log`. + +- The lookup key is `(account, global_sequence)` — the ABI that was in + effect when that specific action executed is used, not the current ABI. +- If the ABI is unavailable (contract not yet captured, ABI store missing, + or the action predates ABI capture), the fields are returned as raw hex + strings. +- Decoding failures are soft: they are logged at `debug` level and the + response falls back to raw hex instead of returning HTTP 500. This + prevents a malformed ABI in one contract from breaking queries for + unrelated actions in the same block. + +When decoded `params` and `return_data` are present they appear alongside the +raw `data` and `return_value` hex fields. + +--- + +## HTTP API reference + +All endpoints accept `POST` with a JSON body. The base URL is +`/v1/trace_api/`. + +--- + +### get_block + +Retrieve the full action trace for a single block. + +**Endpoint:** `POST /v1/trace_api/get_block` + +**Request:** + +```json +{ "block_num": 1000 } +``` + +**Response (200):** + +```json +{ + "id": "000003e8...", + "number": 1000, + "previous_id": "000003e7...", + "status": "irreversible", + "timestamp": "2025-01-01T00:05:00.000Z", + "producer": "bp.one", + "transaction_mroot": "0000...0000", + "finality_mroot": "0000...0000", + "transactions": [ + { + "id": "abcd1234...", + "block_num": 1000, + "block_time": "2025-01-01T00:05:00.000Z", + "producer_block_id": "000003e8...", + "actions": [ + { + "global_sequence": 12345, + "receiver": "sysio.token", + "account": "sysio.token", + "action": "transfer", + "authorization": [{ "account": "alice", "permission": "active" }], + "data": "0000000000855c34...", + "return_value": "", + "params": { + "from": "alice", + "to": "bob", + "quantity": "1.0000 SYS", + "memo": "payment" + } + } + ], + "status": "executed", + "cpu_usage_us": 200, + "net_usage_words": 16, + "signatures": ["SIG_K1_..."], + "transaction_header": { + "expiration": "2025-01-01T00:05:30", + "ref_block_num": 999, + "ref_block_prefix": 12345678, + "max_net_usage_words": 0, + "max_cpu_usage_ms": 0, + "delay_sec": 0 + } + } + ] +} +``` + +**Error responses:** + +| Code | Condition | +|------|-----------| +| 400 | `block_num` missing or not a number | +| 404 | Block not found in trace store | + +--- + +### get_transaction_trace + +Retrieve the trace for a single transaction by its ID. + +**Endpoint:** `POST /v1/trace_api/get_transaction_trace` + +**Request:** + +```json +{ "id": "abcd1234ef567890abcd1234ef567890abcd1234ef567890abcd1234ef567890" } +``` + +The block number is resolved via the per-slice `trx_id` index (O(1)); no +block scanning is performed. + +**Response (200):** A single transaction object in the same shape as one +element of `transactions` in the `get_block` response (includes `id`, +`block_num`, `block_time`, `actions`, `status`, etc.). + +**Error responses:** + +| Code | Condition | +|------|-----------| +| 400 | `id` missing or malformed | +| 404 | Transaction not found in index, or block trace not found | + +--- + +### get_actions + +Paginated search over action traces in a block range, with optional filters +on receiver, account (contract code), and action name. + +**Endpoint:** `POST /v1/trace_api/get_actions` + +**Request fields:** + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `block_num_start` | uint32 | `0` | First block to scan (inclusive). | +| `block_num_end` | uint32 | `UINT32_MAX` | Last block to scan (inclusive). | +| `receiver` | string | *(any)* | Filter: only actions where `receiver` matches. | +| `account` | string | *(any)* | Filter: only actions where `account` (code account) matches. | +| `action` | string | *(any)* | Filter: only actions where the action name matches. | +| `after_global_seq` | uint64 | `0` | Pagination cursor — skip actions with `global_sequence <= this`. | +| `limit` | uint32 | `100` | Maximum results to return (clamped to `trace-max-query-limit`, default 1000). | + +**Request example:** + +```json +{ + "block_num_start": 1, + "block_num_end": 10000, + "account": "sysio.token", + "action": "transfer", + "limit": 50 +} +``` + +**Response (200):** + +```json +{ + "actions": [ + { + "global_sequence": 101, + "receiver": "sysio.token", + "account": "sysio.token", + "action": "transfer", + "data": "0000000000855c34...", + "return_value": "", + "params": { + "from": "alice", + "to": "bob", + "quantity": "1.0000 SYS", + "memo": "payment" + }, + "trx_id": "abcd1234...", + "block_num": 1000, + "block_time": "2025-01-01T00:05:00.000Z", + "producer_block_id": "000003e8..." + } + ], + "more": true, + "last_global_seq": 101 +} +``` + +**Response fields:** + +| Field | Description | +|-------|-------------| +| `actions` | Array of matching action objects. | +| `more` | `true` if there are more results beyond `limit`; use `last_global_seq` as the cursor for the next page. | +| `last_global_seq` | `global_sequence` of the last returned action; pass as `after_global_seq` on the next request. | + +**Action object fields:** + +| Field | Description | +|-------|-------------| +| `global_sequence` | Monotonically increasing sequence number across the entire chain. | +| `receiver` | The account that received (and may have processed) the action. | +| `account` | The contract account whose code was executed. | +| `action` | The action name. | +| `data` | Raw action payload as hex. | +| `return_value` | Raw return value as hex (empty string when none). | +| `params` | ABI-decoded action payload (omitted when ABI unavailable). | +| `return_data` | ABI-decoded return value (omitted when ABI unavailable or no return type defined). | +| `trx_id` | ID of the transaction that contains this action. | +| `block_num` | Block number. | +| `block_time` | Block timestamp (ISO-8601). | +| `producer_block_id` | Block ID as reported by the producer (null for pending blocks). | + +**Error responses:** + +| Code | Condition | +|------|-----------| +| 400 | Malformed request body, or `block_num_start > block_num_end`. | + +#### Receiver vs account + +Every SYSIO action has two account fields: + +- **`account`** — the contract whose code is executed (always the contract + that defines the action). +- **`receiver`** — the account receiving the notification. For the + originating action `receiver == account`. For inline notifications sent to + other accounts, `receiver != account`. + +A `sysio.token::transfer` produces two action traces in the store: + +| global_seq | receiver | account | +|-----------|----------|---------| +| N | `sysio.token` | `sysio.token` | ← original execution | +| N+1 | `bob` | `sysio.token` | ← inline notification to recipient | + +Filter by `receiver="sysio.token"` to get exactly one entry per transfer. +Filter by `account="sysio.token"` to get both. + +--- + +### get_token_transfers + +Convenience wrapper around `get_actions` preset to return only token +`transfer` actions for a given contract. Uses +`receiver=account=token_contract, action=transfer` so exactly one entry per +transfer is returned (the canonical execution; inline notification copies +to recipients are excluded). + +**Endpoint:** `POST /v1/trace_api/get_token_transfers` + +**Request fields:** + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `token_contract` | string | `sysio.token` | Contract account to filter on. | +| `block_num_start` | uint32 | `0` | First block to scan (inclusive). | +| `block_num_end` | uint32 | `UINT32_MAX` | Last block to scan (inclusive). | +| `after_global_seq` | uint64 | `0` | Pagination cursor. | +| `limit` | uint32 | `100` | Maximum results (clamped to 1000). | + +**Request example:** + +```json +{ + "token_contract": "sysio.token", + "block_num_start": 1, + "block_num_end": 50000, + "limit": 100 +} +``` + +**Response (200):** + +```json +{ + "transfers": [ + { + "global_sequence": 101, + "receiver": "sysio.token", + "account": "sysio.token", + "action": "transfer", + "data": "...", + "params": { + "from": "alice", + "to": "bob", + "quantity": "1.0000 SYS", + "memo": "payment" + }, + "trx_id": "abcd1234...", + "block_num": 1000, + "block_time": "2025-01-01T00:05:00.000Z", + "producer_block_id": "000003e8..." + } + ], + "more": false, + "last_global_seq": 101 +} +``` + +The response uses `"transfers"` as the array key instead of `"actions"`. +All other fields are identical to `get_actions`. + +**Error responses:** + +| Code | Condition | +|------|-----------| +| 400 | Malformed request body, or `block_num_start > block_num_end`. | + +--- + +## Pagination guide + +All scan endpoints (`get_actions`, `get_token_transfers`) use a forward +cursor based on `global_sequence`: + +``` +# Page 1 +POST /v1/trace_api/get_actions +{ "account": "sysio.token", "action": "transfer", + "block_num_start": 1, "block_num_end": 1000000, + "limit": 100 } + +# Page 2 — use last_global_seq from page 1 response +POST /v1/trace_api/get_actions +{ "account": "sysio.token", "action": "transfer", + "block_num_start": 1, "block_num_end": 1000000, + "after_global_seq": , + "limit": 100 } +``` + +Continue until `"more": false`. + +Notes: +- `block_num_start` and `block_num_end` must remain the same across pages + of the same scan. +- The cursor is global_sequence-based, not block-based, so pages never + overlap or skip actions even when multiple actions share a block. +- An empty `actions` array with `"more": false` means no matching actions + exist in the range. + +--- + +## Exchange / indexer integration guide + +### Removing the per-request limit + +Exchanges running their own private nodeop should set +`trace-max-query-limit = -1` in `config.ini` to remove the server-side cap +entirely. This allows a single request to return all transfers in a block +range without pagination, which simplifies indexing pipelines that process +blocks in bulk. + +```ini +# config.ini — safe on a private/trusted node +trace-max-query-limit = -1 +``` + +With this setting, the client controls page size through the `limit` field +(or omits it to get all matching results). Do **not** set this on a public +RPC node — unbounded scans over large block ranges can exhaust server memory. + +### Detecting deposits + +To find all incoming transfers to your account (`exchange1111`) on +`sysio.token`: + +```bash +# Scan the last 10000 blocks +curl -s -X POST http://127.0.0.1:8888/v1/trace_api/get_token_transfers \ + -H 'Content-Type: application/json' \ + -d '{ + "block_num_start": 100000, + "block_num_end": 110000, + "limit": 1000 + }' | jq '.transfers[] | select(.params.to == "exchange1111")' +``` + +Using `get_token_transfers` with no additional filter returns one entry per +transfer across all accounts. Filter `params.to` client-side or scan with +`get_actions` and post-filter as needed. + +The `receiver="sysio.token"` preset guarantees that each on-chain transfer +appears exactly once regardless of how many accounts were notified inline. + +### Non-system token contracts + +```bash +curl -s -X POST http://127.0.0.1:8888/v1/trace_api/get_token_transfers \ + -H 'Content-Type: application/json' \ + -d '{ "token_contract": "mytoken1111", "block_num_start": 1, "block_num_end": 9999999 }' +``` + +### Watching for smart-contract activity + +```bash +# All actions executed by a DEX contract in blocks 5000–6000 +curl -s -X POST http://127.0.0.1:8888/v1/trace_api/get_actions \ + -H 'Content-Type: application/json' \ + -d '{ "account": "my.dex", "block_num_start": 5000, "block_num_end": 6000 }' +``` + +### Inline actions + +Inline actions (e.g. `eosio.token::transfer` called from inside another +contract) appear as separate entries in `get_actions` results with their own +`global_sequence` values. The parent and child share the same `trx_id`. +Use `get_block` or `get_transaction_trace` if you need the full causal tree. + +--- + +## Maintenance and retention + +### Automatic retention + +Set `trace-minimum-irreversible-history-blocks` to the number of blocks you +want to retain past LIB. Slices that fall entirely before +`LIB - retention_blocks` are eligible for deletion. + +Set `trace-minimum-uncompressed-irreversible-history-blocks` similarly to +control the compression boundary. Slices are still accessible when +compressed but random-access reads may be slightly slower. + +### Manual deletion + +Stop nodeop before deleting trace files manually. Delete entire slice +triplets (`trace_*`, `trace_index_*`, `trace_trx_idx_*` for the same +range). Partial deletion (e.g. deleting only the trace file but not the +index) will cause `bad_data_exception` errors on the next startup. + +### Snapshot restores + +After restoring from a snapshot the trace store's recorded range may not +match the chain's new head. The plugin logs a warning at startup and +continues. If the gap is large, consider either: + +1. Copying the trace directory from a full-history node that has the missing + range, or +2. Deleting the trace directory entirely and re-syncing from genesis (only + practical for newer chains). + +### abi_store.log + +`abi_store.log` is rewritten after every block. If it is lost or corrupted, +delete it and restart nodeop. The plugin will rebuild it as new `setabi` +transactions are applied; historical ABI lookup for events before the loss +will fall back to raw hex. + +--- + +## Plugin variants + +Two plugin classes are registered: + +| Class | Purpose | +|-------|---------| +| `trace_api_plugin` | Full plugin: captures trace data AND exposes HTTP endpoints. Use this in production. | +| `trace_api_rpc_plugin` | HTTP-only: exposes endpoints against a trace directory written by another node. Use when separating the writer node from the query node. | + +Both accept the same configuration options. diff --git a/tests/TestHarness/Cluster.py b/tests/TestHarness/Cluster.py index 9e86413d21..f924becf3e 100644 --- a/tests/TestHarness/Cluster.py +++ b/tests/TestHarness/Cluster.py @@ -283,7 +283,7 @@ def launch(self, pnodes=1, unstartedNodes=0, totalNodes=1, prodCount=21, topo="m if Utils.Debug and "--contracts-console" not in extraNodeopArgs: nodeopArgs += " --contracts-console" if PFSetupPolicy.hasPreactivateFeature(pfSetupPolicy): - nodeopArgs += " --plugin sysio::producer_api_plugin" + nodeopArgs += " --plugin sysio::producer_api_plugin " if prodsEnableTraceApi: nodeopArgs += " --plugin sysio::trace_api_plugin " httpMaxResponseTimeSet = False diff --git a/tests/nodeop_run_test.py b/tests/nodeop_run_test.py index 92d9daebbd..c4cbf5eb66 100755 --- a/tests/nodeop_run_test.py +++ b/tests/nodeop_run_test.py @@ -299,6 +299,45 @@ if typeVal != "transfer" or amountVal != 975311: errorExit("FAILURE - get transaction trans_id failed: %s %s %s" % (transId, typeVal, amountVal), raw=True) + # ------------------------------------------------------------------------- + # Verify trace_api get_actions and get_token_transfers endpoints + # ------------------------------------------------------------------------- + blockNum = transaction["block_num"] + + Print("Verify trace_api get_actions returns transfer with decoded params") + actResult = node.processUrllibRequest("trace_api", "get_actions", { + "block_num_start": blockNum, + "block_num_end": blockNum, + "account": "sysio.token", + "action": "transfer" + }, silentErrors=False, exitOnError=True) + assert actResult["code"] == 200, f"get_actions returned HTTP {actResult['code']}" + actionsPayload = actResult["payload"] + matchingActions = [a for a in actionsPayload["actions"] if a["trx_id"] == transId] + assert len(matchingActions) >= 1, \ + f"get_actions: expected at least one action for trxid {transId}, got: {actionsPayload}" + assert "params" in matchingActions[0], \ + f"get_actions: expected decoded 'params' field, got: {matchingActions[0]}" + assert "quantity" in matchingActions[0]["params"], \ + f"get_actions: expected 'quantity' in params, got: {matchingActions[0]['params']}" + + Print("Verify trace_api get_token_transfers returns exactly one entry per transfer") + xferResult = node.processUrllibRequest("trace_api", "get_token_transfers", { + "block_num_start": blockNum, + "block_num_end": blockNum + }, silentErrors=False, exitOnError=True) + assert xferResult["code"] == 200, f"get_token_transfers returned HTTP {xferResult['code']}" + xfersPayload = xferResult["payload"] + matchingXfers = [t for t in xfersPayload["transfers"] if t["trx_id"] == transId] + assert len(matchingXfers) == 1, \ + f"get_token_transfers: expected exactly 1 entry for trxid {transId} (receiver filter should exclude inline notifications), got {len(matchingXfers)}: {matchingXfers}" + assert matchingXfers[0]["receiver"] == "sysio.token", \ + f"get_token_transfers: expected receiver 'sysio.token', got: {matchingXfers[0]['receiver']}" + assert "params" in matchingXfers[0], \ + f"get_token_transfers: expected decoded 'params', got: {matchingXfers[0]}" + assert "quantity" in matchingXfers[0]["params"], \ + f"get_token_transfers: expected 'quantity' in params, got: {matchingXfers[0]['params']}" + Print("Currency Contract Tests") Print("verify no contract in place") Print("Get code hash for account %s" % (currencyAccount.name)) From 4d9b22a2f01773e4f27cae17bc7c07b74bc4d5be Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Mon, 13 Apr 2026 17:35:03 -0500 Subject: [PATCH 06/32] trace_api: widen abi_store blob_offset and blob_size to uint64_t Prevent integer overflow in the ABI blob offset accumulator when total ABI data exceeds 4 GB. blob_offset and blob_size in abi_store_index_entry are promoted from uint32_t to uint64_t (struct grows from 24 to 32 bytes); the local running_offset and blob_offsets vector in abi_store_writer::write() follow suit, removing the truncating static_cast casts. --- .../include/sysio/trace_api/abi_store.hpp | 10 +++++----- plugins/trace_api_plugin/src/abi_store.cpp | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/abi_store.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/abi_store.hpp index 1880d76d11..86d07df272 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/abi_store.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/abi_store.hpp @@ -15,8 +15,8 @@ namespace sysio::trace_api { // Single-file layout (written atomically via .tmp + rename): // // Header (16 bytes): magic, version, entry_count, reserved -// Index (entry_count * 24 bytes, sorted by account ASC, global_seq ASC): -// account(8) + global_seq(8) + blob_offset(4) + blob_size(4) +// Index (entry_count * 32 bytes, sorted by account ASC, global_seq ASC): +// account(8) + global_seq(8) + blob_offset(8) + blob_size(8) // blob_offset is relative to the start of the blob area. // Blob area (variable): raw ABI bytes concatenated in index order // @@ -41,10 +41,10 @@ static_assert(sizeof(abi_store_header) == 16); struct abi_store_index_entry { uint64_t account; // chain::name value uint64_t global_seq; - uint32_t blob_offset; // relative to blob area start - uint32_t blob_size; + uint64_t blob_offset; // relative to blob area start + uint64_t blob_size; }; -static_assert(sizeof(abi_store_index_entry) == 24); +static_assert(sizeof(abi_store_index_entry) == 32); // --------------------------------------------------------------------------- // Writer: accumulate (account, global_seq, abi_bytes) triples; write atomically. diff --git a/plugins/trace_api_plugin/src/abi_store.cpp b/plugins/trace_api_plugin/src/abi_store.cpp index fc17db0724..aa24904f60 100644 --- a/plugins/trace_api_plugin/src/abi_store.cpp +++ b/plugins/trace_api_plugin/src/abi_store.cpp @@ -27,11 +27,11 @@ void abi_store_writer::write(const std::filesystem::path& path) const { }); // Compute blob offsets (relative to blob area start). - uint32_t running_offset = 0; - std::vector blob_offsets(order.size()); + uint64_t running_offset = 0; + std::vector blob_offsets(order.size()); for (size_t i = 0; i < order.size(); ++i) { blob_offsets[i] = running_offset; - running_offset += static_cast(_entries[order[i]].abi_bytes.size()); + running_offset += _entries[order[i]].abi_bytes.size(); } const auto tmp_path = std::filesystem::path(path).replace_extension(".tmp"); @@ -53,7 +53,7 @@ void abi_store_writer::write(const std::filesystem::path& path) const { ie.account = e.account; ie.global_seq = e.global_seq; ie.blob_offset = blob_offsets[i]; - ie.blob_size = static_cast(e.abi_bytes.size()); + ie.blob_size = e.abi_bytes.size(); auto ie_data = fc::raw::pack(ie); f.write(ie_data.data(), ie_data.size()); } From fee1815746efc963a0b1c66516b6561f997ac93e Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Mon, 13 Apr 2026 19:10:22 -0500 Subject: [PATCH 07/32] trace_api: expose action receipt fields and drop failed-action handling Implements spring#1438 by capturing and exposing the full set of action receipt and execution-tree fields on every action trace: - action_trace_v0 now carries action_ordinal, creator_action_ordinal, closest_unnotified_ancestor_action_ordinal, recv_sequence, auth_sequence (flat_map), code_sequence, abi_sequence, account_ram_deltas, and optional cpu_usage_us / net_usage (populated for top-level input actions, where producers set deterministic budgets). - authorization_trace_v0: rename account -> actor to match on-chain naming. - account_delta_v0: new struct for account_ram_deltas. - JSON output uses "name" (was "action") for the action name and "actor" (was "account") inside authorization entries. Remove failed-action support: - chain_extraction filters context-free AND failed action traces (at.except set) from stored block traces. - transaction_trace_v0 no longer carries a status field; all persisted transactions are executed. - get_transaction_trace / get_actions / get_token_transfers no longer accept or return a "failed" indicator. Slim response preserved: get_token_transfers returns only transfer-relevant fields (omits ordinals, receipt sequences, ram_deltas, cpu/net usage). Callers needing those can call get_actions directly. Tests and plugin docs updated accordingly. --- .../include/sysio/trace_api/extract_util.hpp | 19 ++- .../sysio/trace_api/request_handler.hpp | 114 +++++++++++--- .../include/sysio/trace_api/trace.hpp | 45 ++++-- .../trace_api_plugin/src/request_handler.cpp | 44 ++++-- .../trace_api_plugin/src/trace_api_plugin.cpp | 2 +- .../include/sysio/trace_api/test_common.hpp | 19 ++- .../test/test_data_handlers.cpp | 75 +++++++--- .../trace_api_plugin/test/test_extraction.cpp | 122 ++++++++------- .../test/test_get_actions.cpp | 14 +- .../trace_api_plugin/test/test_responses.cpp | 141 +++++++++++------- .../trace_api_plugin/test/test_trace_file.cpp | 131 +++++++--------- plugins/trace_api_plugin/trace_api_plugin.md | 51 ++++++- 12 files changed, 503 insertions(+), 274 deletions(-) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/extract_util.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/extract_util.hpp index e4c4a2b1aa..742fa502bf 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/extract_util.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/extract_util.hpp @@ -6,18 +6,30 @@ namespace sysio { namespace trace_api { inline action_trace_v0 to_action_trace( const chain::action_trace& at ) { action_trace_v0 r; + r.action_ordinal = at.action_ordinal; + r.creator_action_ordinal = at.creator_action_ordinal; + r.closest_unnotified_ancestor_action_ordinal = at.closest_unnotified_ancestor_action_ordinal; r.receiver = at.receiver; r.account = at.act.account; r.action = at.act.name; r.data = at.act.data; r.return_value = at.return_value; + r.cpu_usage_us = at.cpu_usage_us; + r.net_usage = at.net_usage; if( at.receipt ) { r.global_sequence = at.receipt->global_sequence; + r.recv_sequence = at.receipt->recv_sequence; + r.auth_sequence = at.receipt->auth_sequence; + r.code_sequence = at.receipt->code_sequence; + r.abi_sequence = at.receipt->abi_sequence; } r.authorization.reserve( at.act.authorization.size()); for( const auto& auth : at.act.authorization ) { r.authorization.emplace_back( authorization_trace_v0{auth.actor, auth.permission} ); } + for( const auto& delta : at.account_ram_deltas ) { + r.account_ram_deltas.emplace_back( account_delta_v0{delta.account, delta.delta} ); + } return r; } @@ -25,7 +37,6 @@ inline transaction_trace_v0 to_transaction_trace( const cache_trace& t ) { transaction_trace_v0 r; r.id = t.trace->id; if (t.trace->receipt) { - r.status = chain::transaction_receipt_header::status_enum::executed; r.cpu_usage_us = t.trace->total_cpu_usage_us; // Round up net_usage and convert to words r.net_usage_words = (t.trace->net_usage + 7)/8; @@ -38,9 +49,9 @@ inline transaction_trace_v0 to_transaction_trace( const cache_trace& t ) { r.actions.reserve( t.trace->action_traces.size()); for( const auto& at : t.trace->action_traces ) { - if( !at.context_free ) { // not including CFA at this time - r.actions.emplace_back( to_action_trace(at) ); - } + if( at.context_free ) continue; // skip context-free actions + if( at.except ) continue; // skip failed actions + r.actions.emplace_back( to_action_trace(at) ); } return r; } diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp index 31842e40c3..e09d22584d 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp @@ -129,6 +129,95 @@ namespace sysio::trace_api { * @return actions_result containing the matching actions and pagination state */ actions_result get_actions(const action_query& query) { + return get_actions_impl(query, [this](const action_trace_v0& a, + const transaction_trace_v0& trx) { + auto action_var = fc::mutable_variant_object() + ("action_ordinal", a.action_ordinal) + ("creator_action_ordinal", a.creator_action_ordinal) + ("closest_unnotified_ancestor_action_ordinal", a.closest_unnotified_ancestor_action_ordinal) + ("global_sequence", a.global_sequence) + ("recv_sequence", a.recv_sequence) + ("auth_sequence", a.auth_sequence) + ("code_sequence", a.code_sequence) + ("abi_sequence", a.abi_sequence) + ("receiver", a.receiver.to_string()) + ("account", a.account.to_string()) + ("name", a.action.to_string()) + ("authorization", serialize_authorizations(a.authorization)) + ("data", a.data.empty() ? "" : fc::to_hex(a.data.data(), a.data.size())) + ("return_value", a.return_value.empty() ? "" : fc::to_hex(a.return_value.data(), a.return_value.size())) + ("trx_id", trx.id.str()) + ("block_num", trx.block_num) + ("block_time", trx.block_time) + ("producer_block_id", trx.producer_block_id); + + // account_ram_deltas + { + fc::variants deltas; + deltas.reserve(a.account_ram_deltas.size()); + for (const auto& d : a.account_ram_deltas) + deltas.emplace_back(fc::mutable_variant_object() + ("account", d.account.to_string()) + ("delta", d.delta)); + action_var("account_ram_deltas", std::move(deltas)); + } + + if (a.cpu_usage_us.has_value()) + action_var("cpu_usage_us", *a.cpu_usage_us); + if (a.net_usage.has_value()) + action_var("net_usage", *a.net_usage); + + auto [params, return_data] = data_handler_provider.serialize_to_variant(a); + if (!params.is_null()) + action_var("params", params); + if (return_data.has_value()) + action_var("return_data", *return_data); + + return action_var; + }); + } + + /// Slim response for get_token_transfers: transfer-relevant fields only. + /// Omits execution-tree ordinals, receipt sequences, ram_deltas, and resource usage. + actions_result get_token_transfer_actions(const action_query& query) { + return get_actions_impl(query, [this](const action_trace_v0& a, + const transaction_trace_v0& trx) { + auto action_var = fc::mutable_variant_object() + ("global_sequence", a.global_sequence) + ("receiver", a.receiver.to_string()) + ("account", a.account.to_string()) + ("name", a.action.to_string()) + ("authorization", serialize_authorizations(a.authorization)) + ("data", a.data.empty() ? "" : fc::to_hex(a.data.data(), a.data.size())) + ("return_value", a.return_value.empty() ? "" : fc::to_hex(a.return_value.data(), a.return_value.size())) + ("trx_id", trx.id.str()) + ("block_num", trx.block_num) + ("block_time", trx.block_time) + ("producer_block_id", trx.producer_block_id); + + auto [params, return_data] = data_handler_provider.serialize_to_variant(a); + if (!params.is_null()) + action_var("params", params); + if (return_data.has_value()) + action_var("return_data", *return_data); + + return action_var; + }); + } + + private: + static fc::variants serialize_authorizations(const std::vector& auths) { + fc::variants result; + result.reserve(auths.size()); + for (const auto& a : auths) + result.emplace_back(fc::mutable_variant_object() + ("actor", a.actor.to_string()) + ("permission", a.permission.to_string())); + return result; + } + + template + actions_result get_actions_impl(const action_query& query, ActionVariantBuilder&& build_action_var) { actions_result result; const uint32_t end = query.block_num_end; @@ -138,8 +227,6 @@ namespace sysio::trace_api { std::visit([&](const auto& bt) { for (const auto& trx : bt.transactions) { - // actions within a transaction are already in global_sequence order after - // process_actions sorts them, but the raw vector may not be — sort here std::vector sorted; sorted.reserve(trx.actions.size()); for (const auto& a : trx.actions) @@ -155,30 +242,12 @@ namespace sysio::trace_api { if (query.account && a.account != *query.account) continue; if (query.action && a.action != *query.action) continue; - auto action_var = fc::mutable_variant_object() - ("global_sequence", a.global_sequence) - ("receiver", a.receiver.to_string()) - ("account", a.account.to_string()) - ("action", a.action.to_string()) - ("data", a.data.empty() ? "" : fc::to_hex(a.data.data(), a.data.size())) - ("return_value", a.return_value.empty() ? "" : fc::to_hex(a.return_value.data(), a.return_value.size())) - ("trx_id", trx.id.str()) - ("block_num", trx.block_num) - ("block_time", trx.block_time) - ("producer_block_id", trx.producer_block_id); - - auto [params, return_data] = data_handler_provider.serialize_to_variant(a); - if (!params.is_null()) - action_var("params", params); - if (return_data.has_value()) - action_var("return_data", *return_data); - result.last_global_seq = a.global_sequence; - result.actions.push_back(std::move(action_var)); + result.actions.push_back(build_action_var(a, trx)); if (result.actions.size() >= query.limit) { result.more = true; - return; // exits the lambda; outer loop will also check result.more + return; } } if (result.more) return; @@ -191,7 +260,6 @@ namespace sysio::trace_api { return result; } - private: LogfileProvider logfile_provider; DataHandlerProvider data_handler_provider; log_handler _log; diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/trace.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/trace.hpp index 8c40ddaf9f..edc2c92fb4 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/trace.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/trace.hpp @@ -3,30 +3,44 @@ #include #include #include +#include #include namespace sysio { namespace trace_api { struct authorization_trace_v0 { - chain::name account; + chain::name actor; chain::name permission; }; + struct account_delta_v0 { + chain::name account; + int64_t delta = 0; + }; + struct action_trace_v0 { - uint64_t global_sequence = {}; - chain::name receiver = {}; - chain::name account = {}; - chain::name action = {}; - std::vector authorization = {}; - chain::bytes data = {}; - chain::bytes return_value = {}; + fc::unsigned_int action_ordinal = {}; + fc::unsigned_int creator_action_ordinal = {}; + fc::unsigned_int closest_unnotified_ancestor_action_ordinal = {}; + uint64_t global_sequence = {}; + uint64_t recv_sequence = {}; + boost::container::flat_map auth_sequence = {}; + fc::unsigned_int code_sequence = {}; + fc::unsigned_int abi_sequence = {}; + chain::name receiver = {}; + chain::name account = {}; + chain::name action = {}; + std::vector authorization = {}; + chain::bytes data = {}; + chain::bytes return_value = {}; + std::vector account_ram_deltas = {}; + std::optional cpu_usage_us = {}; + std::optional net_usage = {}; }; struct transaction_trace_v0 { - using status_type = chain::transaction_receipt_header::status_enum; chain::transaction_id_type id = {}; std::vector actions = {}; - fc::enum_type status = {}; uint32_t cpu_usage_us = 0; fc::unsigned_int net_usage_words; std::vector signatures = {}; @@ -59,9 +73,14 @@ namespace sysio { namespace trace_api { } } -FC_REFLECT(sysio::trace_api::authorization_trace_v0, (account)(permission)) -FC_REFLECT(sysio::trace_api::action_trace_v0, (global_sequence)(receiver)(account)(action)(authorization)(data)(return_value)) -FC_REFLECT(sysio::trace_api::transaction_trace_v0, (id)(actions)(status)(cpu_usage_us)(net_usage_words)(signatures)(trx_header)(block_num)(block_time)(producer_block_id)) +FC_REFLECT(sysio::trace_api::authorization_trace_v0, (actor)(permission)) +FC_REFLECT(sysio::trace_api::account_delta_v0, (account)(delta)) +FC_REFLECT(sysio::trace_api::action_trace_v0, + (action_ordinal)(creator_action_ordinal)(closest_unnotified_ancestor_action_ordinal) + (global_sequence)(recv_sequence)(auth_sequence)(code_sequence)(abi_sequence) + (receiver)(account)(action)(authorization)(data)(return_value) + (account_ram_deltas)(cpu_usage_us)(net_usage)) +FC_REFLECT(sysio::trace_api::transaction_trace_v0, (id)(actions)(cpu_usage_us)(net_usage_words)(signatures)(trx_header)(block_num)(block_time)(producer_block_id)) FC_REFLECT(sysio::trace_api::block_trace_v0, (id)(number)(previous_id)(timestamp)(producer)(transaction_mroot)(finality_mroot)(transactions)) FC_REFLECT(sysio::trace_api::cache_trace, (trace)(trx)) FC_REFLECT(sysio::trace_api::block_trxs_entry, (ids)(block_num)) diff --git a/plugins/trace_api_plugin/src/request_handler.cpp b/plugins/trace_api_plugin/src/request_handler.cpp index c67c6b527f..1514694c5d 100644 --- a/plugins/trace_api_plugin/src/request_handler.cpp +++ b/plugins/trace_api_plugin/src/request_handler.cpp @@ -16,13 +16,12 @@ namespace { result.reserve(authorizations.size()); for ( const auto& a: authorizations) { result.emplace_back(fc::mutable_variant_object() - ("account", a.account.to_string()) + ("actor", a.actor.to_string()) ("permission", a.permission.to_string()) ); } return result; - } fc::variants process_actions(const std::vector& actions, const data_handler_function& data_handler) { @@ -37,14 +36,40 @@ namespace { const auto& a = actions.at(index); auto action_variant = fc::mutable_variant_object(); - action_variant("global_sequence", a.global_sequence) - ("receiver", a.receiver.to_string()) - ("account", a.account.to_string()) - ("action", a.action.to_string()) - ("authorization", process_authorizations(a.authorization)) - ("data", fc::to_hex(a.data.data(), a.data.size())); + action_variant + ("action_ordinal", a.action_ordinal) + ("creator_action_ordinal", a.creator_action_ordinal) + ("closest_unnotified_ancestor_action_ordinal", a.closest_unnotified_ancestor_action_ordinal) + ("global_sequence", a.global_sequence) + ("recv_sequence", a.recv_sequence) + ("auth_sequence", a.auth_sequence) + ("code_sequence", a.code_sequence) + ("abi_sequence", a.abi_sequence) + ("receiver", a.receiver.to_string()) + ("account", a.account.to_string()) + ("name", a.action.to_string()) + ("authorization", process_authorizations(a.authorization)) + ("data", fc::to_hex(a.data.data(), a.data.size())) + ("return_value", fc::to_hex(a.return_value.data(), a.return_value.size())); + + // account_ram_deltas + { + fc::variants deltas; + deltas.reserve(a.account_ram_deltas.size()); + for (const auto& d : a.account_ram_deltas) { + deltas.emplace_back(fc::mutable_variant_object() + ("account", d.account.to_string()) + ("delta", d.delta) + ); + } + action_variant("account_ram_deltas", std::move(deltas)); + } + + if (a.cpu_usage_us.has_value()) + action_variant("cpu_usage_us", *a.cpu_usage_us); + if (a.net_usage.has_value()) + action_variant("net_usage", *a.net_usage); - action_variant("return_value", fc::to_hex(a.return_value.data(), a.return_value.size())); auto [params, return_data] = data_handler(a); if (!params.is_null()) { action_variant("params", params); @@ -69,7 +94,6 @@ namespace { ("block_time", t.block_time) ("producer_block_id", t.producer_block_id) ("actions", process_actions(t.actions, data_handler)) - ("status", t.status) ("cpu_usage_us", t.cpu_usage_us) ("net_usage_words", t.net_usage_words) ("signatures", t.signatures) diff --git a/plugins/trace_api_plugin/src/trace_api_plugin.cpp b/plugins/trace_api_plugin/src/trace_api_plugin.cpp index d21f7b13b8..b75d15e0cd 100644 --- a/plugins/trace_api_plugin/src/trace_api_plugin.cpp +++ b/plugins/trace_api_plugin/src/trace_api_plugin.cpp @@ -416,7 +416,7 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_thisget_actions(query); + auto result = req_handler->get_token_transfer_actions(query); cb( 200, fc::mutable_variant_object() ("transfers", result.actions) ("more", result.more) diff --git a/plugins/trace_api_plugin/test/include/sysio/trace_api/test_common.hpp b/plugins/trace_api_plugin/test/include/sysio/trace_api/test_common.hpp index a602429acf..a572f3ab58 100644 --- a/plugins/trace_api_plugin/test/include/sysio/trace_api/test_common.hpp +++ b/plugins/trace_api_plugin/test/include/sysio/trace_api/test_common.hpp @@ -115,26 +115,39 @@ namespace sysio::trace_api { inline bool operator==(const authorization_trace_v0& lhs, const authorization_trace_v0& rhs) { return - lhs.account == rhs.account && + lhs.actor == rhs.actor && lhs.permission == rhs.permission; } + inline bool operator==(const account_delta_v0& lhs, const account_delta_v0& rhs) { + return lhs.account == rhs.account && lhs.delta == rhs.delta; + } + inline bool operator==(const action_trace_v0& lhs, const action_trace_v0& rhs) { return + lhs.action_ordinal == rhs.action_ordinal && + lhs.creator_action_ordinal == rhs.creator_action_ordinal && + lhs.closest_unnotified_ancestor_action_ordinal == rhs.closest_unnotified_ancestor_action_ordinal && lhs.global_sequence == rhs.global_sequence && + lhs.recv_sequence == rhs.recv_sequence && + lhs.auth_sequence == rhs.auth_sequence && + lhs.code_sequence == rhs.code_sequence && + lhs.abi_sequence == rhs.abi_sequence && lhs.receiver == rhs.receiver && lhs.account == rhs.account && lhs.action == rhs.action && lhs.authorization == rhs.authorization && lhs.data == rhs.data && - lhs.return_value == rhs.return_value; + lhs.return_value == rhs.return_value && + lhs.account_ram_deltas == rhs.account_ram_deltas && + lhs.cpu_usage_us == rhs.cpu_usage_us && + lhs.net_usage == rhs.net_usage; } inline bool operator==(const transaction_trace_v0& lhs, const transaction_trace_v0& rhs) { return lhs.id == rhs.id && lhs.actions == rhs.actions && - lhs.status == rhs.status && lhs.cpu_usage_us == rhs.cpu_usage_us && lhs.net_usage_words == rhs.net_usage_words && lhs.signatures == rhs.signatures && diff --git a/plugins/trace_api_plugin/test/test_data_handlers.cpp b/plugins/trace_api_plugin/test/test_data_handlers.cpp index eefe1b78bf..aa2dea8281 100644 --- a/plugins/trace_api_plugin/test/test_data_handlers.cpp +++ b/plugins/trace_api_plugin/test/test_data_handlers.cpp @@ -27,9 +27,11 @@ namespace { BOOST_AUTO_TEST_SUITE(abi_data_handler_tests) BOOST_AUTO_TEST_CASE(empty_data) { - auto action = action_trace_v0 { - 0, "alice"_n, "alice"_n, "foo"_n, {}, {}, {} - }; + action_trace_v0 action; + action.global_sequence = 0; + action.receiver = "alice"_n; + action.account = "alice"_n; + action.action = "foo"_n; std::variant action_trace_t = action; abi_data_handler handler(exception_handler{}); @@ -44,9 +46,12 @@ BOOST_AUTO_TEST_SUITE(abi_data_handler_tests) { // Without return_value { - auto action = action_trace_v0 { - 0, "alice"_n, "alice"_n, "foo"_n, {}, {0x00, 0x01, 0x02, 0x03}, {} - }; + action_trace_v0 action; + action.global_sequence = 0; + action.receiver = "alice"_n; + action.account = "alice"_n; + action.action = "foo"_n; + action.data = {0x00, 0x01, 0x02, 0x03}; std::variant action_trace_t = action; abi_data_handler handler(exception_handler{}); @@ -59,9 +64,13 @@ BOOST_AUTO_TEST_SUITE(abi_data_handler_tests) // With return_value { - auto action = action_trace_v0 { - 0, "alice"_n, "alice"_n, "foo"_n, {}, {0x00, 0x01, 0x02, 0x03}, {0x04, 0x05, 0x06, 0x07} - }; + action_trace_v0 action; + action.global_sequence = 0; + action.receiver = "alice"_n; + action.account = "alice"_n; + action.action = "foo"_n; + action.data = {0x00, 0x01, 0x02, 0x03}; + action.return_value = {0x04, 0x05, 0x06, 0x07}; std::variant action_trace_t = action; abi_data_handler handler(exception_handler{}); @@ -75,9 +84,12 @@ BOOST_AUTO_TEST_SUITE(abi_data_handler_tests) BOOST_AUTO_TEST_CASE(basic_abi) { - auto action = action_trace_v0 { - 0, "alice"_n, "alice"_n, "foo"_n, {}, {0x00, 0x01, 0x02, 0x03}, {} - }; + action_trace_v0 action; + action.global_sequence = 0; + action.receiver = "alice"_n; + action.account = "alice"_n; + action.action = "foo"_n; + action.data = {0x00, 0x01, 0x02, 0x03}; std::variant action_trace_t = action; @@ -108,9 +120,13 @@ BOOST_AUTO_TEST_SUITE(abi_data_handler_tests) BOOST_AUTO_TEST_CASE(basic_abi_with_return_value) { - auto action = action_trace_v0 { - 0, "alice"_n, "alice"_n, "foo"_n, {}, {0x00, 0x01, 0x02, 0x03}, {0x04, 0x05, 0x06} - }; + action_trace_v0 action; + action.global_sequence = 0; + action.receiver = "alice"_n; + action.account = "alice"_n; + action.action = "foo"_n; + action.data = {0x00, 0x01, 0x02, 0x03}; + action.return_value = {0x04, 0x05, 0x06}; std::variant action_trace_t = action; @@ -148,9 +164,13 @@ BOOST_AUTO_TEST_SUITE(abi_data_handler_tests) BOOST_AUTO_TEST_CASE(basic_abi_wrong_type) { - auto action = action_trace_v0 { - 0, "alice"_n, "alice"_n, "foo"_n, {}, {0x00, 0x01, 0x02, 0x03}, {0x04, 0x05, 0x06, 0x07} - }; + action_trace_v0 action; + action.global_sequence = 0; + action.receiver = "alice"_n; + action.account = "alice"_n; + action.action = "foo"_n; + action.data = {0x00, 0x01, 0x02, 0x03}; + action.return_value = {0x04, 0x05, 0x06, 0x07}; std::variant action_trace_t = action; @@ -177,9 +197,12 @@ BOOST_AUTO_TEST_SUITE(abi_data_handler_tests) BOOST_AUTO_TEST_CASE(basic_abi_insufficient_data) { - auto action = action_trace_v0 { - 0, "alice"_n, "alice"_n, "foo"_n, {}, {0x00, 0x01, 0x02}, {} - }; + action_trace_v0 action; + action.global_sequence = 0; + action.receiver = "alice"_n; + action.account = "alice"_n; + action.action = "foo"_n; + action.data = {0x00, 0x01, 0x02}; std::variant action_trace_t = action; @@ -212,9 +235,13 @@ BOOST_AUTO_TEST_SUITE(abi_data_handler_tests) // If no ABI provided for return type then do not attempt to decode it BOOST_AUTO_TEST_CASE(basic_abi_no_return_abi_when_return_value_provided) { - auto action = action_trace_v0 { - 0, "alice"_n, "alice"_n, "foo"_n, {}, {0x00, 0x01, 0x02, 0x03}, {0x04, 0x05, 0x06} - }; + action_trace_v0 action; + action.global_sequence = 0; + action.receiver = "alice"_n; + action.account = "alice"_n; + action.action = "foo"_n; + action.data = {0x00, 0x01, 0x02, 0x03}; + action.return_value = {0x04, 0x05, 0x06}; std::variant action_trace_t = action; diff --git a/plugins/trace_api_plugin/test/test_extraction.cpp b/plugins/trace_api_plugin/test/test_extraction.cpp index f5ac8be515..fc66c54fae 100644 --- a/plugins/trace_api_plugin/test/test_extraction.cpp +++ b/plugins/trace_api_plugin/test/test_extraction.cpp @@ -169,34 +169,41 @@ BOOST_AUTO_TEST_SUITE(block_extraction) { chain::packed_transaction(ptrx1) } ); signal_accepted_block( bp1 ); - const std::vector expected_action_traces { - { - 0, - "sysio.token"_n, "sysio.token"_n, "transfer"_n, - {{"alice"_n, "active"_n}}, - make_transfer_data("alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!"), - {} - }, - { - 1, - "alice"_n, "sysio.token"_n, "transfer"_n, - {{"alice"_n, "active"_n}}, - make_transfer_data("alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!"), - {} - }, - { - 2, - "bob"_n, "sysio.token"_n, "transfer"_n, - {{"alice"_n, "active"_n}}, - make_transfer_data("alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!"), - {} - } - }; + action_trace_v0 eat1{}; + eat1.global_sequence = 0; + eat1.receiver = "sysio.token"_n; + eat1.account = "sysio.token"_n; + eat1.action = "transfer"_n; + eat1.authorization = {{"alice"_n, "active"_n}}; + eat1.data = make_transfer_data("alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!"); + eat1.cpu_usage_us = fc::unsigned_int{0}; + eat1.net_usage = fc::unsigned_int{0}; + + action_trace_v0 eat2{}; + eat2.global_sequence = 1; + eat2.receiver = "alice"_n; + eat2.account = "sysio.token"_n; + eat2.action = "transfer"_n; + eat2.authorization = {{"alice"_n, "active"_n}}; + eat2.data = make_transfer_data("alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!"); + eat2.cpu_usage_us = fc::unsigned_int{0}; + eat2.net_usage = fc::unsigned_int{0}; + + action_trace_v0 eat3{}; + eat3.global_sequence = 2; + eat3.receiver = "bob"_n; + eat3.account = "sysio.token"_n; + eat3.action = "transfer"_n; + eat3.authorization = {{"alice"_n, "active"_n}}; + eat3.data = make_transfer_data("alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!"); + eat3.cpu_usage_us = fc::unsigned_int{0}; + eat3.net_usage = fc::unsigned_int{0}; + + const std::vector expected_action_traces { eat1, eat2, eat3 }; const transaction_trace_v0 expected_transaction_trace { ptrx1.id(), expected_action_traces, - fc::enum_type{0}, 0, 0, ptrx1.get_signatures(), @@ -255,41 +262,44 @@ BOOST_AUTO_TEST_SUITE(block_extraction) { chain::packed_transaction(ptrx1), chain::packed_transaction(ptrx2), chain::packed_transaction(ptrx3) } ); signal_accepted_block( bp1 ); - const std::vector expected_action_trace1 { - { - 0, - "sysio.token"_n, "sysio.token"_n, "transfer"_n, - {{"alice"_n, "active"_n}}, - make_transfer_data("alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!"), - {} - } - }; - - const std::vector expected_action_trace2 { - { - 1, - "bob"_n, "sysio.token"_n, "transfer"_n, - {{ "bob"_n, "active"_n }}, - make_transfer_data( "bob"_n, "alice"_n, "0.0001 SYS"_t, "Memo!" ), - {} - } - }; - - const std::vector expected_action_trace3 { - { - 2, - "fred"_n, "sysio.token"_n, "transfer"_n, - {{ "fred"_n, "active"_n }}, - make_transfer_data( "fred"_n, "bob"_n, "0.0001 SYS"_t, "Memo!" ), - {} - } - }; + action_trace_v0 eat1{}; + eat1.global_sequence = 0; + eat1.receiver = "sysio.token"_n; + eat1.account = "sysio.token"_n; + eat1.action = "transfer"_n; + eat1.authorization = {{"alice"_n, "active"_n}}; + eat1.data = make_transfer_data("alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!"); + eat1.cpu_usage_us = fc::unsigned_int{0}; + eat1.net_usage = fc::unsigned_int{0}; + + action_trace_v0 eat2{}; + eat2.global_sequence = 1; + eat2.receiver = "bob"_n; + eat2.account = "sysio.token"_n; + eat2.action = "transfer"_n; + eat2.authorization = {{ "bob"_n, "active"_n }}; + eat2.data = make_transfer_data( "bob"_n, "alice"_n, "0.0001 SYS"_t, "Memo!" ); + eat2.cpu_usage_us = fc::unsigned_int{0}; + eat2.net_usage = fc::unsigned_int{0}; + + action_trace_v0 eat3{}; + eat3.global_sequence = 2; + eat3.receiver = "fred"_n; + eat3.account = "sysio.token"_n; + eat3.action = "transfer"_n; + eat3.authorization = {{ "fred"_n, "active"_n }}; + eat3.data = make_transfer_data( "fred"_n, "bob"_n, "0.0001 SYS"_t, "Memo!" ); + eat3.cpu_usage_us = fc::unsigned_int{0}; + eat3.net_usage = fc::unsigned_int{0}; + + const std::vector expected_action_trace1 { eat1 }; + const std::vector expected_action_trace2 { eat2 }; + const std::vector expected_action_trace3 { eat3 }; const std::vector expected_transaction_traces { { ptrx1.id(), expected_action_trace1, - fc::enum_type{chain::transaction_receipt_header::status_enum::executed}, 0, 0, ptrx1.get_signatures(), @@ -301,7 +311,6 @@ BOOST_AUTO_TEST_SUITE(block_extraction) { ptrx2.id(), expected_action_trace2, - fc::enum_type{chain::transaction_receipt_header::status_enum::executed}, 0, 0, ptrx2.get_signatures(), @@ -313,7 +322,6 @@ BOOST_AUTO_TEST_SUITE(block_extraction) { ptrx3.id(), expected_action_trace3, - fc::enum_type{chain::transaction_receipt_header::status_enum::executed}, 0, 0, ptrx3.get_signatures(), @@ -341,4 +349,4 @@ BOOST_AUTO_TEST_SUITE(block_extraction) BOOST_REQUIRE_EQUAL(std::get(data_log.at(0)), expected_block_trace); } -BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/plugins/trace_api_plugin/test/test_get_actions.cpp b/plugins/trace_api_plugin/test/test_get_actions.cpp index e64689d862..2074e3c80d 100644 --- a/plugins/trace_api_plugin/test/test_get_actions.cpp +++ b/plugins/trace_api_plugin/test/test_get_actions.cpp @@ -63,7 +63,13 @@ struct get_actions_fixture { action_trace_v0 make_action(uint64_t seq, chain::name receiver, chain::name account, chain::name act, chain::bytes data = {}) { - return { seq, receiver, account, act, {}, std::move(data), {} }; + action_trace_v0 a{}; + a.global_sequence = seq; + a.receiver = receiver; + a.account = account; + a.action = act; + a.data = std::move(data); + return a; } transaction_trace_v0 make_trx(chain::transaction_id_type id, uint32_t block_num, @@ -71,8 +77,6 @@ transaction_trace_v0 make_trx(chain::transaction_id_type id, uint32_t block_num, transaction_trace_v0 trx; trx.id = id; trx.actions = std::move(actions); - trx.status = fc::enum_type{ - chain::transaction_receipt_header::status_enum::executed}; trx.block_num = block_num; trx.block_time = chain::block_timestamp_type(0); return trx; @@ -199,7 +203,7 @@ BOOST_FIXTURE_TEST_CASE(filter_by_action_name, get_actions_fixture) auto r = get_actions(q); BOOST_REQUIRE_EQUAL(r.actions.size(), 1u); - BOOST_TEST(r.actions[0].get_object()["action"].as_string() == "transfer"); + BOOST_TEST(r.actions[0].get_object()["name"].as_string() == "transfer"); } // limit caps the number of returned results; more=true when there are additional results @@ -371,4 +375,4 @@ BOOST_FIXTURE_TEST_CASE(actions_sorted_by_global_sequence, get_actions_fixture) BOOST_TEST(r.actions[2].get_object()["global_sequence"].as_uint64() == 5u); } -BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/plugins/trace_api_plugin/test/test_responses.cpp b/plugins/trace_api_plugin/test/test_responses.cpp index 6232659470..20a37f51e1 100644 --- a/plugins/trace_api_plugin/test/test_responses.cpp +++ b/plugins/trace_api_plugin/test/test_responses.cpp @@ -110,20 +110,20 @@ BOOST_AUTO_TEST_SUITE(trace_responses) BOOST_FIXTURE_TEST_CASE(basic_block_response, response_test_fixture) { - auto action_trace = action_trace_v0 { - 0, - "receiver"_n, "contract"_n, "action"_n, - {{ "alice"_n, "active"_n }}, - { 0x00, 0x01, 0x02, 0x03 }, - { 0x04, 0x05, 0x06, 0x07 } - }; + action_trace_v0 action_trace{}; + action_trace.global_sequence = 0; + action_trace.receiver = "receiver"_n; + action_trace.account = "contract"_n; + action_trace.action = "action"_n; + action_trace.authorization = {{ "alice"_n, "active"_n }}; + action_trace.data = { 0x00, 0x01, 0x02, 0x03 }; + action_trace.return_value = { 0x04, 0x05, 0x06, 0x07 }; auto transaction_trace = transaction_trace_v0 { "0000000000000000000000000000000000000000000000000000000000000001"_h, std::vector { action_trace }, - fc::enum_type{chain::transaction_receipt_header::status_enum::executed}, 10, 5, std::vector{ chain::signature_type() }, @@ -163,13 +163,19 @@ BOOST_AUTO_TEST_SUITE(trace_responses) ("producer_block_id", fc::variant()) ("actions", fc::variants({ fc::mutable_variant_object() + ("action_ordinal", 0) + ("creator_action_ordinal", 0) + ("closest_unnotified_ancestor_action_ordinal", 0) ("global_sequence", 0) + ("recv_sequence", 0) + ("code_sequence", 0) + ("abi_sequence", 0) ("receiver", "receiver") ("account", "contract") - ("action", "action") + ("name", "action") ("authorization", fc::variants({ fc::mutable_variant_object() - ("account", "alice") + ("actor", "alice") ("permission", "active") })) ("data", "00010203") @@ -179,7 +185,6 @@ BOOST_AUTO_TEST_SUITE(trace_responses) ("return_data", fc::mutable_variant_object() ("hex", "04050607")) })) - ("status", "executed") ("cpu_usage_us", 10) ("net_usage_words", 5) ("signatures", fc::variants({"SIG_K1_111111111111111111111111111111111111111111111111111111111111111116uk5ne"})) @@ -206,6 +211,15 @@ BOOST_AUTO_TEST_SUITE(trace_responses) BOOST_FIXTURE_TEST_CASE(basic_block_response_no_params, response_test_fixture) { + action_trace_v0 inner_action{}; + inner_action.global_sequence = 0; + inner_action.receiver = "receiver"_n; + inner_action.account = "contract"_n; + inner_action.action = "action"_n; + inner_action.authorization = {{ "alice"_n, "active"_n }}; + inner_action.data = { 0x00, 0x01, 0x02, 0x03 }; + inner_action.return_value = { 0x04, 0x05, 0x06, 0x07 }; + auto block_trace = block_trace_v0 { "b000000000000000000000000000000000000000000000000000000000000001"_h, 1, @@ -218,15 +232,8 @@ BOOST_AUTO_TEST_SUITE(trace_responses) { "0000000000000000000000000000000000000000000000000000000000000001"_h, std::vector { - { - 0, - "receiver"_n, "contract"_n, "action"_n, - {{ "alice"_n, "active"_n }}, - { 0x00, 0x01, 0x02, 0x03 }, - { 0x04, 0x05, 0x06, 0x07 } - } + inner_action }, - fc::enum_type{chain::transaction_receipt_header::status_enum::executed}, 10, 5, std::vector{ chain::signature_type() }, @@ -255,19 +262,24 @@ BOOST_AUTO_TEST_SUITE(trace_responses) ("producer_block_id", fc::variant()) ("actions", fc::variants({ fc::mutable_variant_object() + ("action_ordinal", 0) + ("creator_action_ordinal", 0) + ("closest_unnotified_ancestor_action_ordinal", 0) ("global_sequence", 0) + ("recv_sequence", 0) + ("code_sequence", 0) + ("abi_sequence", 0) ("receiver", "receiver") ("account", "contract") - ("action", "action") + ("name", "action") ("authorization", fc::variants({ fc::mutable_variant_object() - ("account", "alice") + ("actor", "alice") ("permission", "active") })) ("data", "00010203") ("return_value", "04050607") })) - ("status", "executed") ("cpu_usage_us", 10) ("net_usage_words", 5) ("signatures", fc::variants({"SIG_K1_111111111111111111111111111111111111111111111111111111111111111116uk5ne"})) @@ -299,34 +311,38 @@ BOOST_AUTO_TEST_SUITE(trace_responses) BOOST_FIXTURE_TEST_CASE(basic_block_response_unsorted, response_test_fixture) { - std::vector actions = { - { - 1, - "receiver"_n, "contract"_n, "action"_n, - {{ "alice"_n, "active"_n }}, - { 0x01, 0x01, 0x01, 0x01 }, - { 0x05, 0x05, 0x05, 0x05 } - }, - { - 0, - "receiver"_n, "contract"_n, "action"_n, - {{ "alice"_n, "active"_n }}, - { 0x00, 0x00, 0x00, 0x00 }, - { 0x04, 0x04, 0x04, 0x04 } - }, - { - 2, - "receiver"_n, "contract"_n, "action"_n, - {{ "alice"_n, "active"_n }}, - { 0x02, 0x02, 0x02, 0x02 }, - { 0x06, 0x06, 0x06, 0x06 } - } - }; + action_trace_v0 at1{}; + at1.global_sequence = 1; + at1.receiver = "receiver"_n; + at1.account = "contract"_n; + at1.action = "action"_n; + at1.authorization = {{ "alice"_n, "active"_n }}; + at1.data = { 0x01, 0x01, 0x01, 0x01 }; + at1.return_value = { 0x05, 0x05, 0x05, 0x05 }; + + action_trace_v0 at0{}; + at0.global_sequence = 0; + at0.receiver = "receiver"_n; + at0.account = "contract"_n; + at0.action = "action"_n; + at0.authorization = {{ "alice"_n, "active"_n }}; + at0.data = { 0x00, 0x00, 0x00, 0x00 }; + at0.return_value = { 0x04, 0x04, 0x04, 0x04 }; + + action_trace_v0 at2{}; + at2.global_sequence = 2; + at2.receiver = "receiver"_n; + at2.account = "contract"_n; + at2.action = "action"_n; + at2.authorization = {{ "alice"_n, "active"_n }}; + at2.data = { 0x02, 0x02, 0x02, 0x02 }; + at2.return_value = { 0x06, 0x06, 0x06, 0x06 }; + + std::vector actions = { at1, at0, at2 }; auto transaction_trace = transaction_trace_v0 { "0000000000000000000000000000000000000000000000000000000000000001"_h, actions, - fc::enum_type{chain::transaction_receipt_header::status_enum::executed}, 10, 5, { chain::signature_type() }, @@ -366,45 +382,62 @@ BOOST_AUTO_TEST_SUITE(trace_responses) ("producer_block_id", fc::variant()) ("actions", fc::variants({ fc::mutable_variant_object() + ("action_ordinal", 0) + ("creator_action_ordinal", 0) + ("closest_unnotified_ancestor_action_ordinal", 0) ("global_sequence", 0) + ("recv_sequence", 0) + ("code_sequence", 0) + ("abi_sequence", 0) ("receiver", "receiver") ("account", "contract") - ("action", "action") + ("name", "action") ("authorization", fc::variants({ fc::mutable_variant_object() - ("account", "alice") + ("actor", "alice") ("permission", "active") })) ("data", "00000000") ("return_value", "04040404") , fc::mutable_variant_object() + ("action_ordinal", 0) + ("creator_action_ordinal", 0) + ("closest_unnotified_ancestor_action_ordinal", 0) ("global_sequence", 1) + ("recv_sequence", 0) + ("code_sequence", 0) + ("abi_sequence", 0) ("receiver", "receiver") ("account", "contract") - ("action", "action") + ("name", "action") ("authorization", fc::variants({ fc::mutable_variant_object() - ("account", "alice") + ("actor", "alice") ("permission", "active") })) ("data", "01010101") ("return_value", "05050505") , fc::mutable_variant_object() + ("action_ordinal", 0) + ("creator_action_ordinal", 0) + ("closest_unnotified_ancestor_action_ordinal", 0) ("global_sequence", 2) + ("recv_sequence", 0) + ("code_sequence", 0) + ("abi_sequence", 0) ("receiver", "receiver") ("account", "contract") - ("action", "action") + ("name", "action") ("authorization", fc::variants({ fc::mutable_variant_object() - ("account", "alice") + ("actor", "alice") ("permission", "active") })) ("data", "02020202") ("return_value", "06060606") })) - ("status", "executed") ("cpu_usage_us", 10) ("net_usage_words", 5) ("signatures", fc::variants({"SIG_K1_111111111111111111111111111111111111111111111111111111111111111116uk5ne"})) @@ -491,4 +524,4 @@ BOOST_AUTO_TEST_SUITE(trace_responses) BOOST_TEST(null_response.is_null()); } -BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/plugins/trace_api_plugin/test/test_trace_file.cpp b/plugins/trace_api_plugin/test/test_trace_file.cpp index 9235af1fa5..635aad4b90 100644 --- a/plugins/trace_api_plugin/test/test_trace_file.cpp +++ b/plugins/trace_api_plugin/test/test_trace_file.cpp @@ -12,34 +12,32 @@ using open_state = slice_directory::open_state; namespace { struct test_fixture { - std::vector actions = { - { - 1, - "receiver"_n, "contract"_n, "action"_n, - {{ "alice"_n, "active"_n }}, - { 0x01, 0x01, 0x01, 0x01 }, - { 0x05, 0x05, 0x05, 0x05 } - }, - { - 0, - "receiver"_n, "contract"_n, "action"_n, - {{ "alice"_n, "active"_n }}, - { 0x00, 0x00, 0x00, 0x00 }, - { 0x04, 0x04, 0x04, 0x04} - }, - { - 2, - "receiver"_n, "contract"_n, "action"_n, - {{ "alice"_n, "active"_n }}, - { 0x02, 0x02, 0x02, 0x02 }, - { 0x06, 0x06, 0x06, 0x06 } - } - }; + std::vector actions = []{ + action_trace_v0 a0, a1, a2; + a0.global_sequence = 1; + a0.receiver = "receiver"_n; a0.account = "contract"_n; a0.action = "action"_n; + a0.authorization = {{ "alice"_n, "active"_n }}; + a0.data = { 0x01, 0x01, 0x01, 0x01 }; + a0.return_value = { 0x05, 0x05, 0x05, 0x05 }; + + a1.global_sequence = 0; + a1.receiver = "receiver"_n; a1.account = "contract"_n; a1.action = "action"_n; + a1.authorization = {{ "alice"_n, "active"_n }}; + a1.data = { 0x00, 0x00, 0x00, 0x00 }; + a1.return_value = { 0x04, 0x04, 0x04, 0x04 }; + + a2.global_sequence = 2; + a2.receiver = "receiver"_n; a2.account = "contract"_n; a2.action = "action"_n; + a2.authorization = {{ "alice"_n, "active"_n }}; + a2.data = { 0x02, 0x02, 0x02, 0x02 }; + a2.return_value = { 0x06, 0x06, 0x06, 0x06 }; + + return std::vector{a0, a1, a2}; + }(); transaction_trace_v0 transaction_trace { "0000000000000000000000000000000000000000000000000000000000000001"_h, actions, - fc::enum_type{chain::transaction_receipt_header::status_enum::executed}, 10, 5, { chain::signature_type() }, @@ -72,48 +70,42 @@ namespace { } }; - const block_trace_v0 bt1 { - "0000000000000000000000000000000000000000000000000000000000000001"_h, - 1, - "0000000000000000000000000000000000000000000000000000000000000003"_h, - chain::block_timestamp_type(1), - "bp.one"_n, - "0000000000000000000000000000000000000000000000000000000000000000"_h, - "0000000000000000000000000000000000000000000000000000000000000000"_h, - { - { - "0000000000000000000000000000000000000000000000000000000000000001"_h, - { - { - 0, - "sysio.token"_n, "sysio.token"_n, "transfer"_n, - {{ "alice"_n, "active"_n }}, - make_transfer_data( "alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!" ), - {} - }, - { - 1, - "alice"_n, "sysio.token"_n, "transfer"_n, - {{ "alice"_n, "active"_n }}, - make_transfer_data( "alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!" ), - {} - }, - { - 2, - "bob"_n, "sysio.token"_n, "transfer"_n, - {{ "alice"_n, "active"_n }}, - make_transfer_data( "alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!" ), - {} - } - }, - fc::enum_type{chain::transaction_receipt_header::status_enum::executed}, - 10, - 5, - std::vector{chain::signature_type()}, - chain::transaction_header{chain::time_point_sec(), 1, 0, 100, 50, 0} - } - } - }; + const block_trace_v0 bt1 = []{ + action_trace_v0 at0, at1, at2; + at0.global_sequence = 0; + at0.receiver = "sysio.token"_n; at0.account = "sysio.token"_n; at0.action = "transfer"_n; + at0.authorization = {{ "alice"_n, "active"_n }}; + at0.data = make_transfer_data( "alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!" ); + + at1.global_sequence = 1; + at1.receiver = "alice"_n; at1.account = "sysio.token"_n; at1.action = "transfer"_n; + at1.authorization = {{ "alice"_n, "active"_n }}; + at1.data = make_transfer_data( "alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!" ); + + at2.global_sequence = 2; + at2.receiver = "bob"_n; at2.account = "sysio.token"_n; at2.action = "transfer"_n; + at2.authorization = {{ "alice"_n, "active"_n }}; + at2.data = make_transfer_data( "alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!" ); + + transaction_trace_v0 trx; + trx.id = "0000000000000000000000000000000000000000000000000000000000000001"_h; + trx.actions = {at0, at1, at2}; + trx.cpu_usage_us = 10; + trx.net_usage_words = 5; + trx.signatures = {chain::signature_type()}; + trx.trx_header = chain::transaction_header{chain::time_point_sec(), 1, 0, 100, 50, 0}; + + block_trace_v0 b; + b.id = "0000000000000000000000000000000000000000000000000000000000000001"_h; + b.number = 1; + b.previous_id = "0000000000000000000000000000000000000000000000000000000000000003"_h; + b.timestamp = chain::block_timestamp_type(1); + b.producer = "bp.one"_n; + b.transaction_mroot = "0000000000000000000000000000000000000000000000000000000000000000"_h; + b.finality_mroot = "0000000000000000000000000000000000000000000000000000000000000000"_h; + b.transactions = {trx}; + return b; + }(); const block_trace_v0 bt2 { "0000000000000000000000000000000000000000000000000000000000000002"_h, @@ -127,7 +119,6 @@ namespace { { "f000000000000000000000000000000000000000000000000000000000000004"_h, {}, - fc::enum_type{chain::transaction_receipt_header::status_enum::executed}, 10, 5, std::vector{chain::signature_type()}, @@ -883,7 +874,6 @@ BOOST_AUTO_TEST_SUITE(slice_tests) transaction_trace_v0 trx_trace1 { trx_id1, actions, - fc::enum_type{chain::transaction_receipt_header::status_enum::executed}, 10, 5, { chain::signature_type() }, @@ -893,7 +883,6 @@ BOOST_AUTO_TEST_SUITE(slice_tests) transaction_trace_v0 trx_trace2 { trx_id2, actions, - fc::enum_type{chain::transaction_receipt_header::status_enum::executed}, 10, 5, { chain::signature_type() }, @@ -996,7 +985,6 @@ BOOST_AUTO_TEST_SUITE(slice_tests) transaction_trace_v0 trx_trace1 { target_trx_id, actions, - fc::enum_type{chain::transaction_receipt_header::status_enum::executed}, 10, 5, { chain::signature_type() }, @@ -1006,7 +994,6 @@ BOOST_AUTO_TEST_SUITE(slice_tests) transaction_trace_v0 trx_trace2 { "0000000000000000000000000000000000000000000000000000000000000002"_h, actions, - fc::enum_type{chain::transaction_receipt_header::status_enum::executed}, 10, 5, { chain::signature_type() }, @@ -1124,8 +1111,6 @@ BOOST_AUTO_TEST_SUITE(slice_tests) transaction_trace_v0 trx_trace1 { target_trx_id, actions, - fc::enum_type{ - chain::transaction_receipt_header::status_enum::executed}, 10, 5, {chain::signature_type()}, diff --git a/plugins/trace_api_plugin/trace_api_plugin.md b/plugins/trace_api_plugin/trace_api_plugin.md index 2323045ed7..8ffdfa6dd9 100644 --- a/plugins/trace_api_plugin/trace_api_plugin.md +++ b/plugins/trace_api_plugin/trace_api_plugin.md @@ -258,13 +258,23 @@ Retrieve the full action trace for a single block. "producer_block_id": "000003e8...", "actions": [ { + "action_ordinal": 1, + "creator_action_ordinal": 0, + "closest_unnotified_ancestor_action_ordinal": 0, "global_sequence": 12345, + "recv_sequence": 55, + "auth_sequence": [["alice", 42]], + "code_sequence": 3, + "abi_sequence": 3, "receiver": "sysio.token", "account": "sysio.token", - "action": "transfer", - "authorization": [{ "account": "alice", "permission": "active" }], + "name": "transfer", + "authorization": [{ "actor": "alice", "permission": "active" }], "data": "0000000000855c34...", "return_value": "", + "account_ram_deltas": [], + "cpu_usage_us": 100, + "net_usage": 16, "params": { "from": "alice", "to": "bob", @@ -273,7 +283,6 @@ Retrieve the full action trace for a single block. } } ], - "status": "executed", "cpu_usage_us": 200, "net_usage_words": 16, "signatures": ["SIG_K1_..."], @@ -364,12 +373,23 @@ on receiver, account (contract code), and action name. { "actions": [ { + "action_ordinal": 1, + "creator_action_ordinal": 0, + "closest_unnotified_ancestor_action_ordinal": 0, "global_sequence": 101, + "recv_sequence": 55, + "auth_sequence": [["alice", 42]], + "code_sequence": 3, + "abi_sequence": 3, "receiver": "sysio.token", "account": "sysio.token", - "action": "transfer", + "name": "transfer", + "authorization": [{"actor": "alice", "permission": "active"}], "data": "0000000000855c34...", "return_value": "", + "account_ram_deltas": [], + "cpu_usage_us": 100, + "net_usage": 16, "params": { "from": "alice", "to": "bob", @@ -399,12 +419,23 @@ on receiver, account (contract code), and action name. | Field | Description | |-------|-------------| +| `action_ordinal` | Position of this action in the transaction's execution tree (1-based). | +| `creator_action_ordinal` | Ordinal of the action that created this one (0 for top-level actions). | +| `closest_unnotified_ancestor_action_ordinal` | Ordinal of the nearest ancestor whose receiver has not already been notified. | | `global_sequence` | Monotonically increasing sequence number across the entire chain. | +| `recv_sequence` | Per-receiver sequence number. | +| `auth_sequence` | Array of `[actor, sequence]` pairs, one per authorizing account. | +| `code_sequence` | Number of times the contract's code has been updated up to this action. | +| `abi_sequence` | Number of times the contract's ABI has been updated up to this action. | | `receiver` | The account that received (and may have processed) the action. | | `account` | The contract account whose code was executed. | -| `action` | The action name. | +| `name` | The action name. | +| `authorization` | Array of `{actor, permission}` objects. | | `data` | Raw action payload as hex. | | `return_value` | Raw return value as hex (empty string when none). | +| `account_ram_deltas` | Array of `{account, delta}` objects capturing RAM allocation changes. | +| `cpu_usage_us` | Producer-set CPU in microseconds (present only for input/top-level actions). | +| `net_usage` | Producer-set NET usage in bytes (present only for input/top-level actions). | | `params` | ABI-decoded action payload (omitted when ABI unavailable). | | `return_data` | ABI-decoded return value (omitted when ABI unavailable or no return type defined). | | `trx_id` | ID of the transaction that contains this action. | @@ -480,8 +511,10 @@ to recipients are excluded). "global_sequence": 101, "receiver": "sysio.token", "account": "sysio.token", - "action": "transfer", + "name": "transfer", + "authorization": [{"actor": "alice", "permission": "active"}], "data": "...", + "return_value": "", "params": { "from": "alice", "to": "bob", @@ -500,7 +533,11 @@ to recipients are excluded). ``` The response uses `"transfers"` as the array key instead of `"actions"`. -All other fields are identical to `get_actions`. +`get_token_transfers` returns a **slim subset** of the fields that `get_actions` returns — it omits execution-tree ordinals +(`action_ordinal`, `creator_action_ordinal`, `closest_unnotified_ancestor_action_ordinal`), per-receipt sequence numbers +(`recv_sequence`, `auth_sequence`, `code_sequence`, `abi_sequence`), `account_ram_deltas`, and the resource usage fields +(`cpu_usage_us`, `net_usage`). These are rarely useful for token-transfer exchange/indexer workflows. If you need them, +call `get_actions` with `receiver=account=, action=transfer` instead. **Error responses:** From ab1229a0d8b29e719f363dc2200582bab68c272f Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Mon, 13 Apr 2026 19:52:15 -0500 Subject: [PATCH 08/32] trace_api: add per-slice block-offset sidecar for O(1) get_block Addresses AntelopeIO/leap#1219: /v1/trace_api/get_block response time varies by block height, with a worst case at the end of each trace-slice-stride window (~200ms for the last block of a 10k-block slice). Root cause: store_provider::get_block scanned trace_index_.log from offset 0, unpacking up to stride-many metadata_log_entry records before finding the target block's offset. Fix: add trace_blk_idx_.log, a flat sparse array of stride-many uint64_t slots indexed by (block_num - slice_base). Each slot holds offset+1 into trace_.log (0 reserved as empty). The sidecar is written synchronously alongside the existing metadata log, pre-allocated on creation, and updated in place on fork re-writes. Read path (get_block): one seek + one read on the sidecar. Falls back to the existing scan when the sidecar is missing or the slot is empty, preserving correctness for unusual states. Uses the in-memory best_known_lib for O(1) irreversibility, replacing the lib-entry scan. Write path (append): adds one 8-byte pwrite per block alongside the existing trace + metadata appends. Trivial cost; file is sparse on Linux (~80KB per slice at default stride). Cleanup: include trace_blk_idx_* in slice rotation; doc updates. --- .../sysio/trace_api/store_provider.hpp | 51 ++++++- .../trace_api_plugin/src/store_provider.cpp | 141 +++++++++++++++--- .../trace_api_plugin/test/test_trace_file.cpp | 78 ++++++---- plugins/trace_api_plugin/trace_api_plugin.md | 33 +++- 4 files changed, 250 insertions(+), 53 deletions(-) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp index b9cec45f08..ff9011f7a4 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp @@ -89,6 +89,29 @@ namespace sysio::trace_api { class store_provider; + // On-disk format: trace_blk_idx_.log + // + // Layout: blk_offset_index_header (16 bytes) followed by a flat array of + // width uint64_t slots. Slot (block_num - slice_base) holds offset+1, + // where offset is the position in trace_.log of that block's trace + // data. Slot value 0 means "not present"; this distinguishes a missing + // block from a block stored at offset 0 (the first block in a slice). + // + // The file is pre-allocated sparse at creation, so any slot write is a + // single 8-byte in-place update. Forks naturally overwrite the slot. + // + // Native-endian, x86_64 Linux only (same convention as other slice files). + struct blk_offset_index_header { + static constexpr uint32_t magic_value = 0x424C4958; // "BLIX" + static constexpr uint32_t current_version = 1; + + uint32_t magic = magic_value; + uint32_t version = current_version; + uint32_t width = 0; // slice width (block count per slice) + uint32_t _reserved = 0; + }; + static_assert(sizeof(blk_offset_index_header) == 16); + /** * Provides access to the slice directory. It is only intended to be used by store_provider * and unit tests. @@ -240,6 +263,26 @@ namespace sysio::trace_api { */ std::optional last_recorded_block() const; + /** + * Record the offset of a block's trace data in trace_.log, via the block-offset + * sidecar trace_blk_idx_.log. Creates the sidecar on first write to a new slice. + * Writes to an existing slot naturally overwrite it (fork re-writes). + */ + void write_block_offset(uint32_t block_height, uint64_t trace_offset) const; + + /** + * O(1) lookup of the trace-log offset for a block via the block-offset sidecar. + * Returns nullopt when the sidecar is missing, the slot is empty, or the file is invalid. + * Callers should fall back to scanning the metadata log in that case. + */ + std::optional lookup_block_offset(uint32_t block_height) const; + + /** + * Current best-known LIB as reported by append_lib. Thread-safe; used by readers to + * determine whether a given block is irreversible without scanning the metadata log. + */ + uint32_t best_known_lib() const; + /** * set the LIB for maintenance * @param lib @@ -275,6 +318,11 @@ namespace sysio::trace_api { // take an open index slice file and verify its header is valid and prepare the file to be appended to (or read from) void validate_existing_index_slice_file(fc::cfile& index_file, open_state state) const; + // Open the block-offset sidecar for a slice; creates and pre-allocates if missing. + // Validates the header on open. Returns false + leaves blk_idx at the sidecar path + // (unopened) if the existing file has a wrong magic/version/width. + bool open_or_create_blk_offset_slice(uint32_t slice_number, fc::cfile& blk_idx) const; + // helper for methods that process irreversible slice files template void process_irreversible_slice_range(uint32_t lib, uint32_t upper_bound_block, std::optional& lower_bound_slice, F&& f); @@ -288,7 +336,7 @@ namespace sysio::trace_api { std::optional _last_indexed_slice; const size_t _compression_seek_point_stride; - std::mutex _maintenance_mtx; + mutable std::mutex _maintenance_mtx; std::condition_variable _maintenance_condition; std::thread _maintenance_thread; bool _maintenance_shutdown{false}; @@ -452,3 +500,4 @@ namespace sysio::trace_api { } FC_REFLECT(sysio::trace_api::slice_directory::index_header, (version)) +FC_REFLECT(sysio::trace_api::blk_offset_index_header, (magic)(version)(width)(_reserved)) diff --git a/plugins/trace_api_plugin/src/store_provider.cpp b/plugins/trace_api_plugin/src/store_provider.cpp index 2d6eba1eec..1acf3096b2 100644 --- a/plugins/trace_api_plugin/src/store_provider.cpp +++ b/plugins/trace_api_plugin/src/store_provider.cpp @@ -10,9 +10,10 @@ namespace { static constexpr const char* _trace_index_prefix = "trace_index_"; static constexpr const char* _trace_trx_id_prefix = "trace_trx_id_"; static constexpr const char* _trace_trx_id_index_prefix = "trace_trx_idx_"; + static constexpr const char* _trace_blk_idx_prefix = "trace_blk_idx_"; static constexpr const char* _trace_ext = ".log"; static constexpr const char* _compressed_trace_ext = ".clog"; - // longest prefix is "trace_trx_idx_" (14), then 10+1+10 digits, then ".clog" extension, then null + // longest prefix is "trace_trx_idx_" or "trace_blk_idx_" (14), then 10+1+10 digits, then ".clog" extension, then null static constexpr int _max_filename_size = std::char_traits::length(_trace_trx_id_index_prefix) + 10 + 1 + 10 + std::char_traits::length(_compressed_trace_ext) + 1; std::string make_filename(const char* slice_prefix, const char* slice_ext, uint32_t slice_number, uint32_t slice_width) { @@ -54,6 +55,11 @@ namespace sysio::trace_api { auto be = metadata_log_entry { block_entry_v0 { .id = bt.id, .number = bt.number, .offset = offset }}; append_store(be, index); + + // Record in the block-offset sidecar for O(1) get_block lookups. Fork re-writes + // overwrite the slot; if this step throws the metadata log remains the source of + // truth and get_block falls back to the linear scan. + _slice_directory.write_block_offset(bt.number, offset); } template void store_provider::append(const block_trace_v0& bt); @@ -78,26 +84,30 @@ namespace sysio::trace_api { } get_block_t store_provider::get_block(uint32_t block_height, const yield_function& yield) { - std::optional trace_offset; - bool irreversible = false; - scan_metadata_log_from(block_height, 0, [&block_height, &trace_offset, &irreversible](const metadata_log_entry& e) -> bool { - if (std::holds_alternative(e)) { - const auto& block = std::get(e); - if (block.number == block_height) { - trace_offset = block.offset; - } - } else if (std::holds_alternative(e)) { - auto lib = std::get(e).lib; - if (lib >= block_height) { - irreversible = true; - return false; + // Fast path: O(1) random-access lookup of the trace offset via the block-offset sidecar. + std::optional trace_offset = _slice_directory.lookup_block_offset(block_height); + + if (!trace_offset) { + // Fallback: scan the metadata log. Covers slices without a sidecar (e.g. corruption, + // missing sidecar file) or the rare case where a block exists in the metadata log but + // the sidecar write was interrupted. + scan_metadata_log_from(block_height, 0, [&block_height, &trace_offset](const metadata_log_entry& e) -> bool { + if (std::holds_alternative(e)) { + const auto& block = std::get(e); + if (block.number == block_height) { + trace_offset = block.offset; + } } - } - return true; - }, yield); + return true; + }, yield); + } + if (!trace_offset) { return get_block_t{}; } + + const bool irreversible = block_height <= _slice_directory.best_known_lib(); + std::optional entry = read_data_log(block_height, *trace_offset); if (!entry) { return get_block_t{}; @@ -478,6 +488,96 @@ namespace sysio::trace_api { _maintenance_condition.notify_one(); } + uint32_t slice_directory::best_known_lib() const { + std::scoped_lock lock(_maintenance_mtx); + return _best_known_lib; + } + + bool slice_directory::open_or_create_blk_offset_slice(uint32_t slice_number, fc::cfile& blk_idx) const { + const bool found = find_slice(_trace_blk_idx_prefix, slice_number, blk_idx, /*open_file=*/false); + if (found) { + // Existing file: open for random-access read/write ("rb+"). + blk_idx.open(fc::cfile::update_rw_mode); + blk_offset_index_header header{}; + try { + blk_idx.seek(0); + blk_idx.read(reinterpret_cast(&header), sizeof(header)); + } catch (...) { + blk_idx.close(); + return false; + } + if (header.magic != blk_offset_index_header::magic_value || + header.version != blk_offset_index_header::current_version || + header.width != _width) { + blk_idx.close(); + return false; + } + return true; + } + + // New file: create with truncate+read/write ("wb+") so subsequent seek()+write() + // behave as random-access (append mode "ab+" would ignore the seek on writes). + blk_idx.open(fc::cfile::truncate_rw_mode); + blk_offset_index_header header{}; + header.width = _width; + blk_idx.seek(0); + blk_idx.write(reinterpret_cast(&header), sizeof(header)); + // Pre-allocate by writing the final byte. Linux fills the gap sparsely. + const uint64_t final_byte = sizeof(blk_offset_index_header) + uint64_t{_width} * sizeof(uint64_t) - 1; + const char zero = 0; + blk_idx.seek(final_byte); + blk_idx.write(&zero, 1); + blk_idx.flush(); + return true; + } + + void slice_directory::write_block_offset(uint32_t block_height, uint64_t trace_offset) const { + const uint32_t slice_number = this->slice_number(block_height); + fc::cfile blk_idx; + if (!open_or_create_blk_offset_slice(slice_number, blk_idx)) { + // Existing sidecar is unusable (wrong magic/version/width). Remove and recreate; + // readers fall back to the metadata-log scan in the meantime. + std::filesystem::remove(blk_idx.get_file_path()); + if (!open_or_create_blk_offset_slice(slice_number, blk_idx)) { + return; + } + } + const uint32_t slot = block_height % _width; + const uint64_t slot_pos = sizeof(blk_offset_index_header) + uint64_t{slot} * sizeof(uint64_t); + const uint64_t encoded = trace_offset + 1; // 0 reserved as "empty" + blk_idx.seek(slot_pos); + blk_idx.write(reinterpret_cast(&encoded), sizeof(encoded)); + blk_idx.flush(); + } + + std::optional slice_directory::lookup_block_offset(uint32_t block_height) const { + const uint32_t slice_number = this->slice_number(block_height); + fc::cfile blk_idx; + const bool found = find_slice(_trace_blk_idx_prefix, slice_number, blk_idx, /*open_file=*/false); + if (!found) return std::nullopt; + + try { + blk_idx.open(fc::cfile::update_rw_mode); + blk_offset_index_header header{}; + blk_idx.seek(0); + blk_idx.read(reinterpret_cast(&header), sizeof(header)); + if (header.magic != blk_offset_index_header::magic_value || + header.version != blk_offset_index_header::current_version || + header.width != _width) { + return std::nullopt; + } + const uint32_t slot = block_height % _width; + const uint64_t slot_pos = sizeof(blk_offset_index_header) + uint64_t{slot} * sizeof(uint64_t); + uint64_t encoded = 0; + blk_idx.seek(slot_pos); + blk_idx.read(reinterpret_cast(&encoded), sizeof(encoded)); + if (encoded == 0) return std::nullopt; + return encoded - 1; + } catch (...) { + return std::nullopt; + } + } + void slice_directory::start_maintenance_thread(log_handler log) { _maintenance_thread = std::thread([this, log=std::move(log)](){ fc::set_thread_name( "trace-mx" ); @@ -575,6 +675,13 @@ namespace sysio::trace_api { std::filesystem::remove(idx_path); } + auto blk_idx_filename = make_filename(_trace_blk_idx_prefix, _trace_ext, slice_to_clean, _width); + const auto blk_idx_path = _slice_dir / blk_idx_filename; + if (std::filesystem::exists(blk_idx_path)) { + log(std::string("Removing: ") + blk_idx_path.generic_string()); + std::filesystem::remove(blk_idx_path); + } + auto ctrace = find_compressed_trace_slice(slice_to_clean, dont_open_file); if (ctrace) { log(std::string("Removing: ") + ctrace->get_file_path().generic_string()); diff --git a/plugins/trace_api_plugin/test/test_trace_file.cpp b/plugins/trace_api_plugin/test/test_trace_file.cpp index 635aad4b90..77a6d126e0 100644 --- a/plugins/trace_api_plugin/test/test_trace_file.cpp +++ b/plugins/trace_api_plugin/test/test_trace_file.cpp @@ -821,46 +821,66 @@ BOOST_AUTO_TEST_SUITE(slice_tests) sp.append(block_trace1); sp.append_lib(1); sp.append(block_trace2); - int count = 0; - get_block_t block1 = sp.get_block(1, [&count]() { - if (++count >= 3) { - throw yield_exception(""); - } - }); + + // get_block uses the trace_blk_idx_.log sidecar for O(1) lookup and does + // not iterate, so the yield callback is not invoked on the fast path. + get_block_t block1 = sp.get_block(1, [](){ BOOST_FAIL("yield must not be called on sidecar fast path"); }); BOOST_REQUIRE(block1); BOOST_REQUIRE(std::get<1>(*block1)); const auto block1_bt = std::get<0>(*block1); BOOST_REQUIRE_EQUAL(std::get(block1_bt), block_trace1); - count = 0; - get_block_t block2 = sp.get_block(5, [&count]() { - if (++count >= 4) { - throw yield_exception(""); - } - }); + get_block_t block2 = sp.get_block(5, [](){ BOOST_FAIL("yield must not be called on sidecar fast path"); }); BOOST_REQUIRE(block2); BOOST_REQUIRE(!std::get<1>(*block2)); const auto block2_bt = std::get<0>(*block2); BOOST_REQUIRE_EQUAL(std::get(block2_bt), block_trace2); - count = 0; - try { - sp.get_block(5,[&count]() { - if (++count >= 3) { - throw yield_exception(""); - } - }); - BOOST_FAIL("Should not have completed scan"); - } catch (const yield_exception& ex) { - } + // Missing block: sidecar slot is empty, we fall back to scanning the metadata log. + get_block_t block_missing = sp.get_block(2); + BOOST_REQUIRE(!block_missing); + } - count = 0; - block2 = sp.get_block(2,[&count]() { - if (++count >= 4) { - throw yield_exception(""); - } - }); - BOOST_REQUIRE(!block2); + // Sidecar must be removed when its slice is cleaned up. + BOOST_FIXTURE_TEST_CASE(test_blk_offset_sidecar_cleanup, test_fixture) + { + fc::temp_directory tempdir; + const uint32_t width = 100; + slice_directory sd(tempdir.path(), width, /*min_irr=*/0u, std::optional(), 0); + + // Create sidecar + matching trace/index for slice 0 so cleanup has something to do. + sd.write_block_offset(1, 0); + fc::cfile f; + sd.find_or_create_trace_slice(0, open_state::read, f); + sd.find_or_create_index_slice(0, open_state::read, f); + + const auto sidecar = tempdir.path() / "trace_blk_idx_0000000000-0000000100.log"; + BOOST_REQUIRE(std::filesystem::exists(sidecar)); + + // Advance LIB several slices past 0 so slice 0 is rotated out. + sd.run_maintenance_tasks(width * 5, [](auto&&){}); + + BOOST_REQUIRE(!std::filesystem::exists(sidecar)); + } + + // Fork re-writes the block to a new offset. The sidecar slot must be overwritten so + // get_block returns the latest (fork-resolved) copy, matching the scan-based "last wins". + BOOST_FIXTURE_TEST_CASE(test_blk_offset_fork_rewrite, test_fixture) + { + fc::temp_directory tempdir; + store_provider sp(tempdir.path(), 100, std::optional(), std::optional(), 0); + + // Different block bodies but same block number. Simulates a fork re-applying block 1. + block_trace_v0 forked = block_trace1; + forked.producer = "bp.two"_n; + + sp.append(block_trace1); + sp.append(forked); + + auto result = sp.get_block(1); + BOOST_REQUIRE(result); + const auto bt = std::get(std::get<0>(*result)); + BOOST_REQUIRE_EQUAL(bt, forked); } // Verify basics of get_trx_block_number() diff --git a/plugins/trace_api_plugin/trace_api_plugin.md b/plugins/trace_api_plugin/trace_api_plugin.md index 8ffdfa6dd9..a5ad43af62 100644 --- a/plugins/trace_api_plugin/trace_api_plugin.md +++ b/plugins/trace_api_plugin/trace_api_plugin.md @@ -108,13 +108,14 @@ All files live inside `trace-dir`. The directory is monitored by ### Slice files Blocks are grouped into contiguous slices of `trace-slice-stride` blocks -each. Each slice is represented by three files that share a common range +each. Each slice is represented by four files that share a common range suffix `-` (zero-padded to 10 digits): | File | Description | |------|-------------| | `trace_-.log` | Serialized `block_trace_v0` records (action data). | -| `trace_index_-.log` | Metadata index: block number → byte offset in the trace file. Enables random access without scanning the full trace file. | +| `trace_index_-.log` | Append-only metadata log of `block_entry_v0` and `lib_entry_v0` records. Source of truth; used as a fallback for `get_block` and to track LIB advancement within the slice. | +| `trace_blk_idx_-.log` | Block-offset sidecar (see below). Enables O(1) `get_block` lookups regardless of the block's position within the slice. | | `trace_trx_idx_-.log` | Transaction-id hash index (see below). | When a slice is compressed the trace file is replaced by: @@ -131,16 +132,35 @@ The index and trx_id index files are not compressed. traces/ trace_0000000000-0000010000.log trace_index_0000000000-0000010000.log + trace_blk_idx_0000000000-0000010000.log trace_trx_idx_0000000000-0000010000.log trace_0000010000-0000020000.log trace_index_0000010000-0000020000.log + trace_blk_idx_0000010000-0000020000.log trace_trx_idx_0000010000-0000020000.log trace_0000020000-0000030000.clog <- compressed trace_index_0000020000-0000030000.log + trace_blk_idx_0000020000-0000030000.log trace_trx_idx_0000020000-0000030000.log abi_store.log ``` +### Block-offset index + +`trace_blk_idx_-.log` is a flat fixed-size array of 64-bit +trace-log offsets, one entry per block in the slice, used by +`/v1/trace_api/get_block` for O(1) block lookups. + +- **Header** (16 bytes): magic `BLIX`, version 1, slice width, reserved. +- **Slots** (8 bytes each): `offset + 1` into `trace_-.log`, + or 0 when the slot is empty. The `+1` encoding reserves 0 as an empty + sentinel since a block's trace data can legitimately live at offset 0. + +The sidecar is written synchronously alongside the metadata log as each +block is persisted. Forks that re-apply the same block number overwrite +the slot naturally. If the sidecar is missing or reports an empty slot, +`get_block` falls back to scanning the metadata log. + ### Transaction-id index `trace_trx_idx_-.log` is a compact open-addressing hash table @@ -661,10 +681,11 @@ compressed but random-access reads may be slightly slower. ### Manual deletion -Stop nodeop before deleting trace files manually. Delete entire slice -triplets (`trace_*`, `trace_index_*`, `trace_trx_idx_*` for the same -range). Partial deletion (e.g. deleting only the trace file but not the -index) will cause `bad_data_exception` errors on the next startup. +Stop nodeop before deleting trace files manually. Delete the full set of +slice files for a given range (`trace_*`, `trace_index_*`, +`trace_blk_idx_*`, `trace_trx_idx_*`). Partial deletion (e.g. deleting +only the trace file but not the index) will cause `bad_data_exception` +errors on the next startup. ### Snapshot restores From 2594089ba7ee6ceef12f052c32da22975d313c98 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 14 Apr 2026 06:04:16 -0500 Subject: [PATCH 09/32] trace_api: replace get_actions limit/cursor pagination with block-range cap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops `limit`, `more`, `last_global_seq`, and `after_global_seq` from action_query and actions_result. Adds `--trace-max-block-range` (default 100) which silently clamps `block_num_end` to `block_num_start + max - 1` in the HTTP handler. Clients paginate by advancing `block_num_start` by `max_block_range` between calls. Removes the unbounded scan that an unauthenticated empty-body request to `/v1/trace_api/get_token_transfers` could previously trigger against block 0..UINT32_MAX. Sort by `global_sequence` inside `get_actions_impl` is retained: chain `action_traces` arrive in schedule order, which is NOT execution order when an action queues both `require_recipient` and inline actions. Iterating without the sort would mix a notification handler's inline ahead of later notifications. Behavior matches `chain_plugin`'s `push_transaction` tree shape. Doc tag-along: also updates the `abi_store.log` layout block to reflect the previously-widened blob_offset/blob_size (uint64) — pre-existing stale doc, fixed while updating other doc sections. --- .../sysio/trace_api/request_handler.hpp | 34 ++-- .../trace_api_plugin/src/trace_api_plugin.cpp | 57 +++--- .../test/test_get_actions.cpp | 177 +++++++++++------- plugins/trace_api_plugin/trace_api_plugin.md | 102 +++++----- 4 files changed, 203 insertions(+), 167 deletions(-) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp index e09d22584d..37299e7aaa 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp @@ -28,8 +28,6 @@ namespace sysio::trace_api { std::optional action; ///< filter by action name (any if unset) uint32_t block_num_start = 0; uint32_t block_num_end = std::numeric_limits::max(); - uint64_t after_global_seq = 0; ///< pagination cursor: skip actions with global_seq <= this - uint32_t limit = 100; ///< max results per request (clamped to 1000 by the handler) }; /** @@ -37,8 +35,6 @@ namespace sysio::trace_api { */ struct actions_result { fc::variants actions; - bool more = false; ///< true if there are more results past 'limit' - uint64_t last_global_seq = 0; ///< global_seq of the last returned action; use as after_global_seq for the next page }; template @@ -116,17 +112,14 @@ namespace sysio::trace_api { } /** - * Scan a block range for action traces matching the given filter and return paginated results. + * Scan a block range for action traces matching the given filter. * * Blocks are scanned in ascending order. Within each block, actions are visited in ascending - * global_sequence order. Scanning stops as soon as 'limit' matching actions are found, and - * 'more' is set to true to signal that the caller should paginate. + * global_sequence order. All matching actions in the (caller-clamped) range are returned; + * the caller is responsible for capping block_num_end so that the response stays bounded. * - * Use the returned 'last_global_seq' as 'after_global_seq' on the next call to continue from - * where this call left off. - * - * @param query - filter and pagination parameters - * @return actions_result containing the matching actions and pagination state + * @param query - filter parameters + * @return actions_result containing the matching actions */ actions_result get_actions(const action_query& query) { return get_actions_impl(query, [this](const action_trace_v0& a, @@ -227,6 +220,13 @@ namespace sysio::trace_api { std::visit([&](const auto& bt) { for (const auto& trx : bt.transactions) { + // trx.actions is stored in schedule order (how the chain's apply_context + // scheduled action slots), which is NOT global_sequence order when an + // action queues both inline actions and require_recipient notifications: + // notifications run before inlines, so the inline's global_sequence is + // higher than later-scheduled notifications'. Sort pointers by + // global_sequence so clients always see execution order (matches + // chain_plugin's push_transaction and the legacy get_block response). std::vector sorted; sorted.reserve(trx.actions.size()); for (const auto& a : trx.actions) @@ -237,24 +237,14 @@ namespace sysio::trace_api { for (const action_trace_v0* ap : sorted) { const auto& a = *ap; - if (a.global_sequence <= query.after_global_seq) continue; if (query.receiver && a.receiver != *query.receiver) continue; if (query.account && a.account != *query.account) continue; if (query.action && a.action != *query.action) continue; - result.last_global_seq = a.global_sequence; result.actions.push_back(build_action_var(a, trx)); - - if (result.actions.size() >= query.limit) { - result.more = true; - return; - } } - if (result.more) return; } }, std::get<0>(*data)); - - if (result.more) break; } return result; diff --git a/plugins/trace_api_plugin/src/trace_api_plugin.cpp b/plugins/trace_api_plugin/src/trace_api_plugin.cpp index b75d15e0cd..d376c996b6 100644 --- a/plugins/trace_api_plugin/src/trace_api_plugin.cpp +++ b/plugins/trace_api_plugin/src/trace_api_plugin.cpp @@ -129,9 +129,11 @@ struct trace_api_common_impl { cfg_options("trace-minimum-uncompressed-irreversible-history-blocks", boost::program_options::value()->default_value(-1), "Number of blocks to ensure are uncompressed past LIB. Compressed \"slice\" files are still accessible but may carry a performance loss on retrieval\n" "A value of -1 indicates that automatic compression of \"slice\" files will be turned off."); - cfg_options("trace-max-query-limit", bpo::value()->default_value(1000), - "Maximum number of results returned by a single get_actions or get_token_transfers request.\n" - "A value of -1 removes the limit (use with caution on public nodes)."); + cfg_options("trace-max-block-range", bpo::value()->default_value(100), + "Maximum number of blocks scanned by a single get_actions or get_token_transfers request.\n" + "block_num_end is silently clamped to block_num_start + this - 1.\n" + "Clients paginate by advancing block_num_start by this amount each call.\n" + "A value of -1 removes the cap (use with caution on public nodes)."); } void plugin_initialize(const appbase::variables_map& options) { @@ -160,11 +162,11 @@ struct trace_api_common_impl { minimum_uncompressed_irreversible_history_blocks = uncompressed_blocks; } - const int32_t query_limit = options.at("trace-max-query-limit").as(); - SYS_ASSERT(query_limit >= -1, chain::plugin_config_exception, - "\"trace-max-query-limit\" must be -1 (unlimited) or a positive value."); - max_query_limit = (query_limit == -1) ? std::numeric_limits::max() - : static_cast(query_limit); + const int32_t block_range = options.at("trace-max-block-range").as(); + SYS_ASSERT(block_range == -1 || block_range > 0, chain::plugin_config_exception, + "\"trace-max-block-range\" must be -1 (unlimited) or a positive value."); + max_block_range = (block_range == -1) ? std::numeric_limits::max() + : static_cast(block_range); store = std::make_shared( trace_dir, @@ -195,7 +197,7 @@ struct trace_api_common_impl { static constexpr int32_t manual_slice_file_value = -1; static constexpr uint32_t compression_seek_point_stride = 6 * 1024 * 1024; // 6 MiB strides for clog seek points - uint32_t max_query_limit = 1000; + uint32_t max_block_range = 100; std::shared_ptr store; }; @@ -211,7 +213,7 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_thismax_query_limit; + max_block_range = common->max_block_range; auto store = common->store; auto data_handler = std::make_shared( [](const exception_with_context& e) { @@ -343,10 +345,6 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_this(); if (obj.contains("block_num_end")) query.block_num_end = obj["block_num_end"].as(); - if (obj.contains("after_global_seq")) - query.after_global_seq = obj["after_global_seq"].as_uint64(); - if (obj.contains("limit")) - query.limit = std::min(obj["limit"].as(), max_query_limit); } catch (...) { error_results results{400, "Bad request body"}; cb( 400, fc::variant( results )); @@ -360,13 +358,11 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_thisget_actions(query); - cb( 200, fc::mutable_variant_object() - ("actions", result.actions) - ("more", result.more) - ("last_global_seq", result.last_global_seq) - ); + cb( 200, fc::mutable_variant_object()("actions", result.actions) ); } catch (...) { http_plugin::handle_exception("trace_api", "get_actions", body, cb); } @@ -395,10 +391,6 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_this(); if (obj.contains("block_num_end")) query.block_num_end = obj["block_num_end"].as(); - if (obj.contains("after_global_seq")) - query.after_global_seq = obj["after_global_seq"].as_uint64(); - if (obj.contains("limit")) - query.limit = std::min(obj["limit"].as(), max_query_limit); } catch (...) { error_results results{400, "Bad request body"}; cb( 400, fc::variant( results )); @@ -415,13 +407,11 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_thisget_token_transfer_actions(query); - cb( 200, fc::mutable_variant_object() - ("transfers", result.actions) - ("more", result.more) - ("last_global_seq", result.last_global_seq) - ); + cb( 200, fc::mutable_variant_object()("transfers", result.actions) ); } catch (...) { http_plugin::handle_exception("trace_api", "get_token_transfers", body, cb); } @@ -431,8 +421,17 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_this::max()) return; + const uint64_t max_end = uint64_t{query.block_num_start} + max_block_range - 1; + if (max_end < query.block_num_end) + query.block_num_end = static_cast(max_end); + } + std::shared_ptr common; - uint32_t max_query_limit = 1000; + uint32_t max_block_range = 100; using request_handler_t = request_handler, abi_data_handler::shared_provider>; std::shared_ptr req_handler; diff --git a/plugins/trace_api_plugin/test/test_get_actions.cpp b/plugins/trace_api_plugin/test/test_get_actions.cpp index 2074e3c80d..7619fdfadd 100644 --- a/plugins/trace_api_plugin/test/test_get_actions.cpp +++ b/plugins/trace_api_plugin/test/test_get_actions.cpp @@ -116,8 +116,6 @@ BOOST_FIXTURE_TEST_CASE(empty_range, get_actions_fixture) auto r = get_actions(q); BOOST_TEST(r.actions.empty()); - BOOST_TEST(!r.more); - BOOST_TEST(r.last_global_seq == 0u); } // Filter that matches nothing returns empty result @@ -135,7 +133,6 @@ BOOST_FIXTURE_TEST_CASE(no_matching_filter, get_actions_fixture) auto r = get_actions(q); BOOST_TEST(r.actions.empty()); - BOOST_TEST(!r.more); } // receiver filter: keeps only actions where receiver matches @@ -159,8 +156,6 @@ BOOST_FIXTURE_TEST_CASE(filter_by_receiver, get_actions_fixture) BOOST_REQUIRE_EQUAL(r.actions.size(), 1u); BOOST_TEST(r.actions[0].get_object()["receiver"].as_string() == "sysio.token"); BOOST_TEST(r.actions[0].get_object()["global_sequence"].as_uint64() == 1u); - BOOST_TEST(!r.more); - BOOST_TEST(r.last_global_seq == 1u); } // account filter: keeps only actions where account (code) matches @@ -206,58 +201,6 @@ BOOST_FIXTURE_TEST_CASE(filter_by_action_name, get_actions_fixture) BOOST_TEST(r.actions[0].get_object()["name"].as_string() == "transfer"); } -// limit caps the number of returned results; more=true when there are additional results -BOOST_FIXTURE_TEST_CASE(pagination_limit, get_actions_fixture) -{ - blocks[1] = make_block(1, { - make_trx(TRX1, 1, { - make_action(1, "a"_n, "tok"_n, "transfer"_n), - make_action(2, "a"_n, "tok"_n, "transfer"_n), - make_action(3, "a"_n, "tok"_n, "transfer"_n), - make_action(4, "a"_n, "tok"_n, "transfer"_n), - make_action(5, "a"_n, "tok"_n, "transfer"_n) - }) - }); - - action_query q; - q.block_num_start = 1; - q.block_num_end = 1; - q.limit = 3; - - auto r = get_actions(q); - - BOOST_REQUIRE_EQUAL(r.actions.size(), 3u); - BOOST_TEST(r.more); - BOOST_TEST(r.last_global_seq == 3u); -} - -// after_global_seq skips already-seen actions for cursor-based pagination -BOOST_FIXTURE_TEST_CASE(pagination_after_global_seq, get_actions_fixture) -{ - blocks[1] = make_block(1, { - make_trx(TRX1, 1, { - make_action(1, "a"_n, "tok"_n, "transfer"_n), - make_action(2, "a"_n, "tok"_n, "transfer"_n), - make_action(3, "a"_n, "tok"_n, "transfer"_n), - make_action(4, "a"_n, "tok"_n, "transfer"_n), - make_action(5, "a"_n, "tok"_n, "transfer"_n) - }) - }); - - action_query q; - q.block_num_start = 1; - q.block_num_end = 1; - q.after_global_seq = 3; // skip seq 1-3 - - auto r = get_actions(q); - - BOOST_REQUIRE_EQUAL(r.actions.size(), 2u); - BOOST_TEST(r.actions[0].get_object()["global_sequence"].as_uint64() == 4u); - BOOST_TEST(r.actions[1].get_object()["global_sequence"].as_uint64() == 5u); - BOOST_TEST(!r.more); - BOOST_TEST(r.last_global_seq == 5u); -} - // Actions are returned across multiple blocks; missing blocks in the log are skipped BOOST_FIXTURE_TEST_CASE(multi_block_scan, get_actions_fixture) { @@ -276,8 +219,6 @@ BOOST_FIXTURE_TEST_CASE(multi_block_scan, get_actions_fixture) BOOST_TEST(r.actions[0].get_object()["block_num"].as() == 1u); BOOST_TEST(r.actions[1].get_object()["block_num"].as() == 2u); BOOST_TEST(r.actions[2].get_object()["block_num"].as() == 4u); - BOOST_TEST(!r.more); - BOOST_TEST(r.last_global_seq == 3u); } // ABI-decoded params are included in the result when the data handler returns them @@ -351,15 +292,19 @@ BOOST_FIXTURE_TEST_CASE(token_transfer_filter_excludes_notifications, get_action BOOST_TEST(r.actions[0].get_object()["receiver"].as_string() == "sysio.token"); } -// Actions within a transaction are visited in ascending global_sequence order -// regardless of their storage order in the vector +// Actions within a transaction are returned sorted by global_sequence (execution order), +// not the schedule order in which the chain stored them. The divergence matters when +// an action queues both an inline AND a require_recipient notification — notifications +// execute before inlines, so the inline's global_sequence is higher than later-scheduled +// notifications'. Sorting gives clients a consistent execution view matching what +// chain_plugin's push_transaction does and what the legacy get_block response returned. BOOST_FIXTURE_TEST_CASE(actions_sorted_by_global_sequence, get_actions_fixture) { blocks[1] = make_block(1, { make_trx(TRX1, 1, { - make_action(5, "a"_n, "tok"_n, "transfer"_n), // out of order - make_action(1, "a"_n, "tok"_n, "transfer"_n), - make_action(3, "a"_n, "tok"_n, "transfer"_n) + make_action(5, "a"_n, "tok"_n, "transfer"_n), // schedule order: 5 + make_action(1, "a"_n, "tok"_n, "transfer"_n), // schedule order: 1 + make_action(3, "a"_n, "tok"_n, "transfer"_n) // schedule order: 3 }) }); @@ -375,4 +320,108 @@ BOOST_FIXTURE_TEST_CASE(actions_sorted_by_global_sequence, get_actions_fixture) BOOST_TEST(r.actions[2].get_object()["global_sequence"].as_uint64() == 5u); } +// Realistic scenario exercising top-level actions, notifications (receiver != account), +// inline actions triggered by a notification handler, and the inline's own notifications. +// +// Scenario: alice transfers 1 SYS to bob.contract via sysio.token::transfer. +// bob.contract has a notif handler that fires an inline logger::log action. +// logger::log has an audit account notification handler. +// +// Expected global_sequence order (chain-assigned, monotonic): +// 100: sysio.token::transfer (receiver=sysio.token, account=sysio.token) <- original +// 101: sysio.token::transfer (receiver=alice, account=sysio.token) <- notif to sender +// 102: sysio.token::transfer (receiver=bob.contract,account=sysio.token) <- notif to recipient +// 103: logger::log (receiver=logger, account=logger) <- inline from 102 +// 104: logger::log (receiver=audit, account=logger) <- notif from 103 +BOOST_FIXTURE_TEST_CASE(complex_inline_and_notification_ordering, get_actions_fixture) +{ + blocks[1] = make_block(1, { + make_trx(TRX1, 1, { + make_action(100, "sysio.token"_n, "sysio.token"_n, "transfer"_n), + make_action(101, "alice"_n, "sysio.token"_n, "transfer"_n), + make_action(102, "bob.contract"_n, "sysio.token"_n, "transfer"_n), + make_action(103, "logger"_n, "logger"_n, "log"_n), + make_action(104, "audit"_n, "logger"_n, "log"_n) + }) + }); + + // No filter: all 5 actions in global_sequence order. + { + action_query q; + q.block_num_start = 1; + q.block_num_end = 1; + + auto r = get_actions(q); + + BOOST_REQUIRE_EQUAL(r.actions.size(), 5u); + BOOST_TEST(r.actions[0].get_object()["global_sequence"].as_uint64() == 100u); + BOOST_TEST(r.actions[1].get_object()["global_sequence"].as_uint64() == 101u); + BOOST_TEST(r.actions[2].get_object()["global_sequence"].as_uint64() == 102u); + BOOST_TEST(r.actions[3].get_object()["global_sequence"].as_uint64() == 103u); + BOOST_TEST(r.actions[4].get_object()["global_sequence"].as_uint64() == 104u); + } + + // receiver=sysio.token: only the original execution; notifications to alice/bob excluded. + { + action_query q; + q.block_num_start = 1; + q.block_num_end = 1; + q.receiver = "sysio.token"_n; + + auto r = get_actions(q); + + BOOST_REQUIRE_EQUAL(r.actions.size(), 1u); + BOOST_TEST(r.actions[0].get_object()["global_sequence"].as_uint64() == 100u); + BOOST_TEST(r.actions[0].get_object()["receiver"].as_string() == "sysio.token"); + } + + // account=sysio.token: original + both notifications (3 rows), ordered by global_seq. + { + action_query q; + q.block_num_start = 1; + q.block_num_end = 1; + q.account = "sysio.token"_n; + + auto r = get_actions(q); + + BOOST_REQUIRE_EQUAL(r.actions.size(), 3u); + BOOST_TEST(r.actions[0].get_object()["global_sequence"].as_uint64() == 100u); + BOOST_TEST(r.actions[1].get_object()["global_sequence"].as_uint64() == 101u); + BOOST_TEST(r.actions[2].get_object()["global_sequence"].as_uint64() == 102u); + BOOST_TEST(r.actions[0].get_object()["receiver"].as_string() == "sysio.token"); + BOOST_TEST(r.actions[1].get_object()["receiver"].as_string() == "alice"); + BOOST_TEST(r.actions[2].get_object()["receiver"].as_string() == "bob.contract"); + } + + // account=logger: the inline from bob.contract's notif handler + its notification to audit. + { + action_query q; + q.block_num_start = 1; + q.block_num_end = 1; + q.account = "logger"_n; + + auto r = get_actions(q); + + BOOST_REQUIRE_EQUAL(r.actions.size(), 2u); + BOOST_TEST(r.actions[0].get_object()["global_sequence"].as_uint64() == 103u); + BOOST_TEST(r.actions[1].get_object()["global_sequence"].as_uint64() == 104u); + BOOST_TEST(r.actions[0].get_object()["receiver"].as_string() == "logger"); + BOOST_TEST(r.actions[1].get_object()["receiver"].as_string() == "audit"); + } + + // receiver=bob.contract + action=transfer: exactly the one notification to the recipient. + { + action_query q; + q.block_num_start = 1; + q.block_num_end = 1; + q.receiver = "bob.contract"_n; + q.action = "transfer"_n; + + auto r = get_actions(q); + + BOOST_REQUIRE_EQUAL(r.actions.size(), 1u); + BOOST_TEST(r.actions[0].get_object()["global_sequence"].as_uint64() == 102u); + } +} + BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/plugins/trace_api_plugin/trace_api_plugin.md b/plugins/trace_api_plugin/trace_api_plugin.md index a5ad43af62..54d11067df 100644 --- a/plugins/trace_api_plugin/trace_api_plugin.md +++ b/plugins/trace_api_plugin/trace_api_plugin.md @@ -82,7 +82,7 @@ default). | `trace-slice-stride` | `10000` | Number of blocks per slice file. Larger values reduce file count but increase the amount of data re-scanned when a single slice is accessed. | | `trace-minimum-irreversible-history-blocks` | `-1` | Blocks past LIB to retain before old slices can be auto-deleted. `-1` disables automatic deletion (keep forever). | | `trace-minimum-uncompressed-irreversible-history-blocks` | `-1` | Blocks past LIB to keep uncompressed. Slices older than this threshold are transparently compressed. `-1` disables automatic compression. | -| `trace-max-query-limit` | `1000` | Maximum number of results a single `get_actions` or `get_token_transfers` request may return. Client-supplied `limit` values are clamped to this. Set to `-1` to remove the server-side cap entirely. | +| `trace-max-block-range` | `100` | Maximum number of blocks scanned by a single `get_actions` or `get_token_transfers` request. `block_num_end` is silently clamped to `block_num_start + trace-max-block-range - 1`. Clients paginate by advancing `block_num_start` by this amount on each call. Set to `-1` to remove the cap entirely. | ### Recommended production settings @@ -185,9 +185,9 @@ started (or since the file was first written). Format: ``` -Header (16 bytes): magic "ABIB", version 1, entry_count, reserved -Index (entry_count × 24 bytes, sorted account ASC, global_seq ASC): - account(u64) | global_seq(u64) | blob_offset(u32) | blob_size(u32) +Header (16 bytes): magic "ABIB" (u32), version 1 (u32), entry_count (u64) +Index (entry_count × 32 bytes, sorted account ASC, global_seq ASC): + account(u64) | global_seq(u64) | blob_offset(u64) | blob_size(u64) Blob area (variable): raw fc::raw-packed abi_def bytes in index order ``` @@ -368,12 +368,15 @@ on receiver, account (contract code), and action name. | Field | Type | Default | Description | |-------|------|---------|-------------| | `block_num_start` | uint32 | `0` | First block to scan (inclusive). | -| `block_num_end` | uint32 | `UINT32_MAX` | Last block to scan (inclusive). | +| `block_num_end` | uint32 | `UINT32_MAX` | Last block to scan (inclusive). Silently clamped server-side to `block_num_start + trace-max-block-range - 1`. | | `receiver` | string | *(any)* | Filter: only actions where `receiver` matches. | | `account` | string | *(any)* | Filter: only actions where `account` (code account) matches. | | `action` | string | *(any)* | Filter: only actions where the action name matches. | -| `after_global_seq` | uint64 | `0` | Pagination cursor — skip actions with `global_sequence <= this`. | -| `limit` | uint32 | `100` | Maximum results to return (clamped to `trace-max-query-limit`, default 1000). | + +Returned actions within a transaction are sorted by `global_sequence` +(execution order), matching the behavior of `get_block` and chain_plugin's +`push_transaction` response. See the Pagination guide below for the cursor +pattern. **Request example:** @@ -382,8 +385,7 @@ on receiver, account (contract code), and action name. "block_num_start": 1, "block_num_end": 10000, "account": "sysio.token", - "action": "transfer", - "limit": 50 + "action": "transfer" } ``` @@ -421,9 +423,7 @@ on receiver, account (contract code), and action name. "block_time": "2025-01-01T00:05:00.000Z", "producer_block_id": "000003e8..." } - ], - "more": true, - "last_global_seq": 101 + ] } ``` @@ -431,9 +431,7 @@ on receiver, account (contract code), and action name. | Field | Description | |-------|-------------| -| `actions` | Array of matching action objects. | -| `more` | `true` if there are more results beyond `limit`; use `last_global_seq` as the cursor for the next page. | -| `last_global_seq` | `global_sequence` of the last returned action; pass as `after_global_seq` on the next request. | +| `actions` | Array of matching action objects, ordered by `(block_num, global_sequence)`. | **Action object fields:** @@ -507,9 +505,7 @@ to recipients are excluded). |-------|------|---------|-------------| | `token_contract` | string | `sysio.token` | Contract account to filter on. | | `block_num_start` | uint32 | `0` | First block to scan (inclusive). | -| `block_num_end` | uint32 | `UINT32_MAX` | Last block to scan (inclusive). | -| `after_global_seq` | uint64 | `0` | Pagination cursor. | -| `limit` | uint32 | `100` | Maximum results (clamped to 1000). | +| `block_num_end` | uint32 | `UINT32_MAX` | Last block to scan (inclusive). Silently clamped server-side to `block_num_start + trace-max-block-range - 1`. | **Request example:** @@ -517,8 +513,7 @@ to recipients are excluded). { "token_contract": "sysio.token", "block_num_start": 1, - "block_num_end": 50000, - "limit": 100 + "block_num_end": 50000 } ``` @@ -546,9 +541,7 @@ to recipients are excluded). "block_time": "2025-01-01T00:05:00.000Z", "producer_block_id": "000003e8..." } - ], - "more": false, - "last_global_seq": 101 + ] } ``` @@ -569,54 +562,60 @@ call `get_actions` with `receiver=account=, action=transfer` ins ## Pagination guide -All scan endpoints (`get_actions`, `get_token_transfers`) use a forward -cursor based on `global_sequence`: +`get_actions` and `get_token_transfers` cap the per-request scan window at +`trace-max-block-range` blocks (default 100). `block_num_end` is silently +clamped to `block_num_start + trace-max-block-range - 1` if the client asks +for more. Within that window, ALL matching actions are returned — there +is no per-result limit and no in-window cursor. + +To page across a wide range, advance `block_num_start` by +`trace-max-block-range` on each call: ``` -# Page 1 +# Page 1: blocks 1..100 (assuming trace-max-block-range = 100) POST /v1/trace_api/get_actions { "account": "sysio.token", "action": "transfer", - "block_num_start": 1, "block_num_end": 1000000, - "limit": 100 } + "block_num_start": 1, "block_num_end": 1000000 } -# Page 2 — use last_global_seq from page 1 response +# Page 2: blocks 101..200 POST /v1/trace_api/get_actions { "account": "sysio.token", "action": "transfer", - "block_num_start": 1, "block_num_end": 1000000, - "after_global_seq": , - "limit": 100 } + "block_num_start": 101, "block_num_end": 1000000 } ``` -Continue until `"more": false`. +Continue until the response `actions` array is empty AND +`block_num_start > tail of recorded data`. Use the `get_block` endpoint or +out-of-band knowledge of head block to know when to stop. Notes: -- `block_num_start` and `block_num_end` must remain the same across pages - of the same scan. -- The cursor is global_sequence-based, not block-based, so pages never - overlap or skip actions even when multiple actions share a block. -- An empty `actions` array with `"more": false` means no matching actions - exist in the range. +- Within each transaction, actions are sorted by `global_sequence` + (execution order, not schedule order). See "Receiver vs account" for why + this matters when an action queues both inlines and notifications. +- Operators on private/trusted nodes who want to scan large windows in a + single request can set `trace-max-block-range = -1` in config.ini to + remove the cap entirely. Do NOT set this on a public RPC node. --- ## Exchange / indexer integration guide -### Removing the per-request limit +### Widening the per-request scan window -Exchanges running their own private nodeop should set -`trace-max-query-limit = -1` in `config.ini` to remove the server-side cap -entirely. This allows a single request to return all transfers in a block -range without pagination, which simplifies indexing pipelines that process -blocks in bulk. +Exchanges running their own private nodeop can raise +`trace-max-block-range` in `config.ini` to reduce the number of round +trips per backfill: ```ini # config.ini — safe on a private/trusted node -trace-max-query-limit = -1 +trace-max-block-range = 1000 ``` -With this setting, the client controls page size through the `limit` field -(or omits it to get all matching results). Do **not** set this on a public -RPC node — unbounded scans over large block ranges can exhaust server memory. +`1000` is a sane upper bound for most workloads. Larger values risk +producing multi-MB responses that hit HTTP body / response-time limits +(`http-max-response-time-ms`, `http-max-body-size`) and tie up an HTTP +thread for seconds at a time on busy contracts. `-1` (unlimited) is +available for fully trusted operator-only deployments but is not +recommended even on private nodes — pick an explicit cap instead. ### Detecting deposits @@ -624,13 +623,12 @@ To find all incoming transfers to your account (`exchange1111`) on `sysio.token`: ```bash -# Scan the last 10000 blocks +# Scan a 1000-block window (assumes trace-max-block-range = 1000) curl -s -X POST http://127.0.0.1:8888/v1/trace_api/get_token_transfers \ -H 'Content-Type: application/json' \ -d '{ "block_num_start": 100000, - "block_num_end": 110000, - "limit": 1000 + "block_num_end": 100999 }' | jq '.transfers[] | select(.params.to == "exchange1111")' ``` From 7883aff179097602830aee8f9a2505b577dd4a7e Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 14 Apr 2026 06:05:24 -0500 Subject: [PATCH 10/32] trace_api: cache abi_serializer per (account, global_seq) Bulk get_actions queries over a single contract were rebuilding the ABI serializer per action: a 100-action response triggered 100 abi_def unpacks and 100 abi_serializer constructions. abi_serializer construction walks all types in the ABI and is non-trivial. Adds a 128-entry LRU keyed by (account.value, global_seq) protected by a mutex. Cache misses do the expensive abi_def unpack and abi_serializer construction OUTSIDE the lock so concurrent cache users do not block on a slow miss. On a race where two threads miss for the same key, the second observer of the inserted entry wins and the duplicate construction is discarded. --- .../sysio/trace_api/abi_data_handler.hpp | 35 ++++++++- .../trace_api_plugin/src/abi_data_handler.cpp | 71 ++++++++++++++----- 2 files changed, 86 insertions(+), 20 deletions(-) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp index 588a588bd8..b23675624e 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp @@ -1,7 +1,11 @@ #pragma once #include +#include +#include +#include #include +#include #include #include #include @@ -22,6 +26,9 @@ namespace sysio { * by the abi_store on disk. Given (account, global_sequence) it returns the raw * ABI bytes that were in effect at that point in chain history, or nullopt. * + * A bounded LRU caches constructed abi_serializers by (account, global_seq) so + * bulk queries over the same contract do not rebuild the serializer per action. + * * Can be used directly as a Data_handler_provider OR shared between request_handlers * using the ::shared_provider abstraction. */ @@ -31,9 +38,13 @@ namespace sysio { /// Called on the HTTP thread; must be thread-safe. using abi_lookup_fn = std::function>(chain::name, uint64_t)>; - explicit abi_data_handler( exception_handler except_handler, abi_lookup_fn lookup_fn = {} ) + static constexpr size_t default_cache_capacity = 128; + + explicit abi_data_handler( exception_handler except_handler, abi_lookup_fn lookup_fn = {}, + size_t cache_capacity = default_cache_capacity ) :_abi_lookup_fn( std::move( lookup_fn ) ) ,except_handler( std::move( except_handler ) ) + ,_cache_capacity( cache_capacity ) {} /** @@ -64,7 +75,27 @@ namespace sysio { }; private: - abi_lookup_fn _abi_lookup_fn; + // Look up or construct the abi_serializer in effect for (account, global_seq). + // Returns nullptr if no ABI is available or construction failed. + std::shared_ptr get_serializer(chain::name account, uint64_t global_seq); + + using cache_key = std::pair; + struct cache_key_hash { + size_t operator()(const cache_key& k) const noexcept { + return std::hash{}(k.first) ^ (std::hash{}(k.second) << 1); + } + }; + + abi_lookup_fn _abi_lookup_fn; exception_handler except_handler; + + // LRU cache of (account, global_seq) -> abi_serializer. + // _cache_list: MRU at front, LRU at back. _cache_map: key -> iterator into list. + std::mutex _cache_mtx; + std::list>> _cache_list; + std::unordered_map _cache_map; + const size_t _cache_capacity; }; } } diff --git a/plugins/trace_api_plugin/src/abi_data_handler.cpp b/plugins/trace_api_plugin/src/abi_data_handler.cpp index 34980ba7bc..9760154d7d 100644 --- a/plugins/trace_api_plugin/src/abi_data_handler.cpp +++ b/plugins/trace_api_plugin/src/abi_data_handler.cpp @@ -4,26 +4,61 @@ namespace sysio::trace_api { - std::tuple> abi_data_handler::serialize_to_variant(const std::variant& action) { - return std::visit([&](const auto& a) -> std::tuple> { - if (!_abi_lookup_fn) { - return {}; - } + std::shared_ptr abi_data_handler::get_serializer(chain::name account, uint64_t global_seq) { + const cache_key key{account.to_uint64_t(), global_seq}; - auto abi_bytes = _abi_lookup_fn(a.account, a.global_sequence); - if (!abi_bytes || abi_bytes->empty()) { - return {}; + { + std::lock_guard lock(_cache_mtx); + auto it = _cache_map.find(key); + if (it != _cache_map.end()) { + // Move to MRU (front). splice is O(1) on std::list. + _cache_list.splice(_cache_list.begin(), _cache_list, it->second); + return it->second->second; } + } - try { - chain::abi_def abi; - auto ds = fc::datastream(abi_bytes->data(), abi_bytes->size()); - fc::raw::unpack(ds, abi); + // Miss: look up ABI bytes and build the serializer outside the lock to avoid + // blocking other cache users during a potentially slow file read / unpack. + if (!_abi_lookup_fn) return nullptr; + + auto abi_bytes = _abi_lookup_fn(account, global_seq); + if (!abi_bytes || abi_bytes->empty()) return nullptr; - chain::abi_serializer serializer(std::move(abi), - chain::abi_serializer::create_yield_function(fc::microseconds::maximum())); + std::shared_ptr serializer; + try { + chain::abi_def abi; + auto ds = fc::datastream(abi_bytes->data(), abi_bytes->size()); + fc::raw::unpack(ds, abi); + serializer = std::make_shared(std::move(abi), + chain::abi_serializer::create_yield_function(fc::microseconds::maximum())); + } catch (...) { + except_handler(MAKE_EXCEPTION_WITH_CONTEXT(std::current_exception())); + return nullptr; + } - auto type_name = serializer.get_action_type(a.action); + // Insert into cache. Another thread may have raced us — if so, return that entry. + std::lock_guard lock(_cache_mtx); + auto it = _cache_map.find(key); + if (it != _cache_map.end()) { + _cache_list.splice(_cache_list.begin(), _cache_list, it->second); + return it->second->second; + } + _cache_list.emplace_front(key, serializer); + _cache_map.emplace(key, _cache_list.begin()); + while (_cache_list.size() > _cache_capacity) { + _cache_map.erase(_cache_list.back().first); + _cache_list.pop_back(); + } + return serializer; + } + + std::tuple> abi_data_handler::serialize_to_variant(const std::variant& action) { + return std::visit([&](const auto& a) -> std::tuple> { + auto serializer = get_serializer(a.account, a.global_sequence); + if (!serializer) return {}; + + try { + auto type_name = serializer->get_action_type(a.action); if (type_name.empty()) { return {}; } @@ -37,11 +72,11 @@ namespace sysio::trace_api { }; std::optional ret_data; - auto params = serializer.binary_to_variant(type_name, a.data, abi_yield); + auto params = serializer->binary_to_variant(type_name, a.data, abi_yield); if (a.return_value.size() > 0) { - auto return_type_name = serializer.get_action_result_type(a.action); + auto return_type_name = serializer->get_action_result_type(a.action); if (!return_type_name.empty()) { - ret_data = serializer.binary_to_variant(return_type_name, a.return_value, abi_yield); + ret_data = serializer->binary_to_variant(return_type_name, a.return_value, abi_yield); } } return {std::move(params), std::move(ret_data)}; From 0c3f4ef540184ea912bf0fa0d862e4353db23dde Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 14 Apr 2026 06:06:09 -0500 Subject: [PATCH 11/32] trace_api: mmap abi_store_reader and widen entry_count to uint64_t The reader was re-opening the file on every lookup and trusting cached _blob_area_offset. Between the writer's atomic rename and the swap of the shared_ptr, an in-flight HTTP-thread reader holding the OLD reader (OLD index, OLD blob_area_offset) could resolve the path to the NEW inode and seek to an offset computed against the old layout inside the new file, returning garbage bytes. Holding the file via boost::iostreams::mapped_file_source pins the inode through the rename, removes the per-lookup syscalls (open + seek + read + close), and lets us bounds-check the blob slice against the mapping. The on-disk layout matches the in-memory struct on x86_64 (fc::raw::pack is little-endian, structs are unpadded), so we memcpy the header and index out of the mapping at construction. Widens abi_store_header::entry_count to uint64_t and drops _reserved. Header stays 16 bytes via 4+4+8 layout. Format version intentionally not bumped: chain has not launched. --- .../include/sysio/trace_api/abi_store.hpp | 20 ++-- plugins/trace_api_plugin/src/abi_store.cpp | 95 +++++++++++-------- 2 files changed, 66 insertions(+), 49 deletions(-) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/abi_store.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/abi_store.hpp index 86d07df272..b92905e884 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/abi_store.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/abi_store.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -33,8 +34,7 @@ struct abi_store_header { uint32_t magic = magic_value; uint32_t version = current_version; - uint32_t entry_count = 0; - uint32_t _reserved = 0; + uint64_t entry_count = 0; }; static_assert(sizeof(abi_store_header) == 16); @@ -71,7 +71,11 @@ class abi_store_writer { // --------------------------------------------------------------------------- // Reader: load the index from disk, answer (account, global_seq) lookups. -// The index is held in memory; ABI bytes are read from the file on demand. +// The file is mmap'd for the lifetime of the reader so that: +// - blob reads are zero-copy memcpy from the page cache (no syscall per query), +// - a concurrent writer's atomic rename swaps the path to a new inode without +// invalidating this reader's view (we still hold the old inode via mmap). +// The index is copied into a vector for binary_search. // --------------------------------------------------------------------------- class abi_store_reader { @@ -85,13 +89,13 @@ class abi_store_reader { std::optional> lookup(chain::name account, uint64_t global_seq) const; private: - std::filesystem::path _path; - std::vector _index; // sorted by (account, global_seq) - uint64_t _blob_area_offset{0}; - bool _valid{false}; + boost::iostreams::mapped_file_source _file; + std::vector _index; // sorted by (account, global_seq) + uint64_t _blob_area_offset{0}; + bool _valid{false}; }; } // namespace sysio::trace_api -FC_REFLECT(sysio::trace_api::abi_store_header, (magic)(version)(entry_count)(_reserved)) +FC_REFLECT(sysio::trace_api::abi_store_header, (magic)(version)(entry_count)) FC_REFLECT(sysio::trace_api::abi_store_index_entry, (account)(global_seq)(blob_offset)(blob_size)) diff --git a/plugins/trace_api_plugin/src/abi_store.cpp b/plugins/trace_api_plugin/src/abi_store.cpp index aa24904f60..c8f1bb513d 100644 --- a/plugins/trace_api_plugin/src/abi_store.cpp +++ b/plugins/trace_api_plugin/src/abi_store.cpp @@ -42,7 +42,7 @@ void abi_store_writer::write(const std::filesystem::path& path) const { // Header abi_store_header hdr; - hdr.entry_count = static_cast(order.size()); + hdr.entry_count = order.size(); auto hdr_data = fc::raw::pack(hdr); f.write(hdr_data.data(), hdr_data.size()); @@ -75,39 +75,57 @@ void abi_store_writer::write(const std::filesystem::path& path) const { // abi_store_reader // --------------------------------------------------------------------------- -abi_store_reader::abi_store_reader(const std::filesystem::path& path) - : _path(path) { +abi_store_reader::abi_store_reader(const std::filesystem::path& path) { + if (!std::filesystem::exists(path)) + return; + try { - fc::cfile f; - f.set_file_path(path); - f.open("rb"); - f.seek(0); - auto ds = f.create_datastream(); + _file.open(path.string()); + } catch (...) { + wlog("trace_api: failed to mmap abi_store from {}", path.generic_string()); + return; + } + if (!_file.is_open() || _file.size() < sizeof(abi_store_header)) { + wlog("trace_api: abi_store {} too small for header, ignoring", path.generic_string()); + return; + } - abi_store_header hdr; - fc::raw::unpack(ds, hdr); + // fc::raw::pack uses little-endian for fixed-size integers and the structs + // are packed plain (no padding). On x86_64 (little-endian native) the on-disk + // bytes match the in-memory struct layout, so reinterpret_cast is safe. + // The trace_api_plugin is x86_64 Linux only; documented in the slice-file + // header comments throughout this directory. + const char* base = _file.data(); + abi_store_header hdr; + std::memcpy(&hdr, base, sizeof(hdr)); - if (hdr.magic != abi_store_header::magic_value) { - wlog("trace_api: abi_store {} has wrong magic, ignoring", path.generic_string()); - return; - } - if (hdr.version != abi_store_header::current_version) { - wlog("trace_api: abi_store {} has unsupported version {}, ignoring", - path.generic_string(), hdr.version); - return; - } + if (hdr.magic != abi_store_header::magic_value) { + wlog("trace_api: abi_store {} has wrong magic, ignoring", path.generic_string()); + return; + } + if (hdr.version != abi_store_header::current_version) { + wlog("trace_api: abi_store {} has unsupported version {}, ignoring", + path.generic_string(), hdr.version); + return; + } - _index.resize(hdr.entry_count); - for (auto& e : _index) - fc::raw::unpack(ds, e); + const uint64_t expected_min_size = sizeof(abi_store_header) + + static_cast(hdr.entry_count) * sizeof(abi_store_index_entry); + if (_file.size() < expected_min_size) { + wlog("trace_api: abi_store {} truncated (size {} < expected min {}), ignoring", + path.generic_string(), _file.size(), expected_min_size); + return; + } - // Blob area starts immediately after header + index. - _blob_area_offset = sizeof(abi_store_header) + - static_cast(hdr.entry_count) * sizeof(abi_store_index_entry); - _valid = true; - } catch (...) { - wlog("trace_api: failed to load abi_store from {}", path.generic_string()); + _index.resize(hdr.entry_count); + if (hdr.entry_count > 0) { + std::memcpy(_index.data(), + base + sizeof(abi_store_header), + static_cast(hdr.entry_count) * sizeof(abi_store_index_entry)); } + + _blob_area_offset = expected_min_size; + _valid = true; } std::optional> abi_store_reader::lookup(chain::name account, uint64_t global_seq) const { @@ -131,21 +149,16 @@ std::optional> abi_store_reader::lookup(chain::name account, u if (it->account != acct) return std::nullopt; - std::optional> result; - result.emplace(it->blob_size); - try { - if (it->blob_size > 0) { - fc::cfile f; - f.set_file_path(_path); - f.open("rb"); - f.seek(static_cast(_blob_area_offset + it->blob_offset)); - f.read(result->data(), it->blob_size); - } - } catch (...) { - wlog("trace_api: failed to read ABI blob from {}", _path.generic_string()); + if (it->blob_size == 0) + return std::vector{}; + + const uint64_t blob_start = _blob_area_offset + it->blob_offset; + if (blob_start + it->blob_size > _file.size()) { + // Truncated or corrupt index — refuse rather than read past the mapping. return std::nullopt; } - return result; + const char* blob_ptr = _file.data() + blob_start; + return std::vector(blob_ptr, blob_ptr + it->blob_size); } // --------------------------------------------------------------------------- From c32e9216413aa3370670a2a97c646f0fd2c2a116 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 14 Apr 2026 08:05:36 -0500 Subject: [PATCH 12/32] trace_api: replace abi_store with append-only abi_log Rewriting the entire abi_store.log on every captured ABI was O(N^2) during fresh-start replay: each first-encountered contract triggered a full sort and rewrite of the growing file (plus a reload of the mmap-backed reader). With hundreds to thousands of distinct contracts touched in early blocks, the extraction thread was spending seconds on file I/O per block. Replaces it with an append-only log: Header (16 bytes): magic "ABIL" (u32), version (u32), reserved (u64) Records: account(u64) + global_seq(u64) + blob_size(u64) + blob_bytes + crc32 Appends stream to the tail under a mutex. The lookup index lives in memory as std::map<(account, global_seq), {offset, size}>, built by scanning the file at startup and validating each record's CRC32. Lookups take the index mutex only long enough to copy (offset, size), then pread the blob from the held cfile's fileno -- concurrent lookups never serialize on I/O. Writes are not fsync'd. Tail corruption from a kernel crash is handled by the startup recovery scan: any CRC failure truncates the file at the first bad record. Lost entries are recaptured the next time their contract is touched (setabi observation or lazy current-ABI fetch), so no replay is required. Separate mutexes for appending vs index so lookups do not block on a concurrent append's file write. CRC uses boost::crc_32_type, matching the convention in fc::datastream_crc and finalizer.hpp. Format version intentionally not bumped beyond 1: chain has not launched, so any existing abi_store.log files from earlier builds of this branch can be deleted before upgrading. No auto-migration code. --- .../sysio/trace_api/abi_data_handler.hpp | 2 +- .../include/sysio/trace_api/abi_log.hpp | 114 +++++++ .../include/sysio/trace_api/abi_store.hpp | 101 ------ .../include/sysio/trace_api/metadata_log.hpp | 6 +- .../sysio/trace_api/store_provider.hpp | 12 +- plugins/trace_api_plugin/src/abi_log.cpp | 239 +++++++++++++ plugins/trace_api_plugin/src/abi_store.cpp | 201 ----------- .../trace_api_plugin/src/store_provider.cpp | 16 +- plugins/trace_api_plugin/test/CMakeLists.txt | 2 +- .../trace_api_plugin/test/test_abi_log.cpp | 313 ++++++++++++++++++ .../trace_api_plugin/test/test_abi_store.cpp | 247 -------------- .../trace_api_plugin/test/test_extraction.cpp | 2 +- plugins/trace_api_plugin/trace_api_plugin.md | 46 +-- 13 files changed, 706 insertions(+), 595 deletions(-) create mode 100644 plugins/trace_api_plugin/include/sysio/trace_api/abi_log.hpp delete mode 100644 plugins/trace_api_plugin/include/sysio/trace_api/abi_store.hpp create mode 100644 plugins/trace_api_plugin/src/abi_log.cpp delete mode 100644 plugins/trace_api_plugin/src/abi_store.cpp create mode 100644 plugins/trace_api_plugin/test/test_abi_log.cpp delete mode 100644 plugins/trace_api_plugin/test/test_abi_store.cpp diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp index b23675624e..313ef6a85c 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp @@ -23,7 +23,7 @@ namespace sysio { * Data Handler that uses sysio::chain::abi_serializer to decode action data. * * ABIs are resolved dynamically via an abi_lookup_fn callback, typically backed - * by the abi_store on disk. Given (account, global_sequence) it returns the raw + * by the abi_log on disk. Given (account, global_sequence) it returns the raw * ABI bytes that were in effect at that point in chain history, or nullopt. * * A bounded LRU caches constructed abi_serializers by (account, global_seq) so diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/abi_log.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/abi_log.hpp new file mode 100644 index 0000000000..8d85aab217 --- /dev/null +++ b/plugins/trace_api_plugin/include/sysio/trace_api/abi_log.hpp @@ -0,0 +1,114 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sysio::trace_api { + +// --------------------------------------------------------------------------- +// abi_log: append-only on-disk log of ABI records with an in-memory sorted +// index keyed by (account, global_sequence). +// +// Appends stream records to the end of the file with no rewrite of +// existing records. The lookup index lives in memory and is rebuilt by +// scanning the file at startup. +// +// On-disk format (fc::raw-packed little-endian, x86_64 Linux): +// +// Header (16 bytes): magic "ABIL" (u32), version 1 (u32), reserved (u64) +// Records (repeated until EOF): +// account (u64) +// global_seq (u64) +// blob_size (u64) +// blob_bytes (blob_size bytes) +// crc32 (u32) over (account, global_seq, blob_size, blob_bytes) +// +// Writes are not fsync'd; the on-disk tail may lose the last few records on +// a kernel crash. On startup we scan records, validate CRCs, and truncate +// the file at the first invalid record. Lost records can be rebuilt by +// replaying setabi + lazy-fetch against the chain. +// +// Thread-safety: +// - append() takes _append_mtx for the cfile write, releases it, then +// takes _index_mtx to insert into the lookup index. Two mutexes so +// lookups never wait for file I/O on a concurrent append. +// - lookup() takes _index_mtx briefly to copy (blob_offset, blob_size), +// releases it, then pread()s the blob bytes. pread is atomic w.r.t. +// the fd's shared position and safe to issue concurrently. +// --------------------------------------------------------------------------- + +struct abi_log_header { + static constexpr uint32_t magic_value = 0x414C4942; + static constexpr uint32_t current_version = 1; + + uint32_t magic = magic_value; + uint32_t version = current_version; + uint64_t _reserved = 0; +}; +static_assert(sizeof(abi_log_header) == 16); + +class abi_log { +public: + explicit abi_log(const std::filesystem::path& path); + + abi_log(const abi_log&) = delete; + abi_log& operator=(const abi_log&) = delete; + + bool valid() const { return _valid; } + + // Append a new ABI record. Thread-safe. Last-write-wins for duplicate + // (account, global_seq) keys. + void append(chain::name account, uint64_t global_seq, std::vector abi_bytes); + + // Look up the ABI in effect for account at the largest recorded + // global_seq <= the query. Returns nullopt if no record matches. + // Thread-safe; may run concurrently with append(). + std::optional> lookup(chain::name account, uint64_t global_seq) const; + +private: + // Fixed-size record header on disk: (account, global_seq, blob_size). + // Trailer is a u32 crc32 over (header + blob_bytes). + struct record_header { + uint64_t account = 0; + uint64_t global_seq = 0; + uint64_t blob_size = 0; + }; + static_assert(sizeof(record_header) == 24); + + struct index_entry { + uint64_t blob_offset = 0; // file offset of blob_bytes (not the record_header) + uint64_t blob_size = 0; + }; + using index_key = std::pair; + + // Walk the log file from the header onwards. Returns the offset of the + // end of the last valid record. The file is truncated at that offset if + // any trailing bytes were discarded due to CRC failure or truncation. + uint64_t recover_from_disk(const std::filesystem::path& path); + + static uint32_t compute_record_crc(const record_header& hdr, const char* blob, uint64_t blob_size); + + // _append_mtx serializes the cfile write + _end_offset update. + // _index_mtx guards _index. Separate so lookups never block on a + // concurrent append's file I/O. Append acquires _append_mtx first and + // always releases it before acquiring _index_mtx, so no deadlock is + // possible (lookups only take _index_mtx). + std::mutex _append_mtx; + mutable std::mutex _index_mtx; + std::map _index; + fc::cfile _cfile; // held open for appends; reads go through fileno() + pread + uint64_t _end_offset{0}; + bool _valid{false}; +}; + +} // namespace sysio::trace_api + +FC_REFLECT(sysio::trace_api::abi_log_header, (magic)(version)(_reserved)) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/abi_store.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/abi_store.hpp deleted file mode 100644 index b92905e884..0000000000 --- a/plugins/trace_api_plugin/include/sysio/trace_api/abi_store.hpp +++ /dev/null @@ -1,101 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -namespace sysio::trace_api { - -// --------------------------------------------------------------------------- -// On-disk format: abi_store.log -// -// Single-file layout (written atomically via .tmp + rename): -// -// Header (16 bytes): magic, version, entry_count, reserved -// Index (entry_count * 32 bytes, sorted by account ASC, global_seq ASC): -// account(8) + global_seq(8) + blob_offset(8) + blob_size(8) -// blob_offset is relative to the start of the blob area. -// Blob area (variable): raw ABI bytes concatenated in index order -// -// Serialized with fc::raw (native-endian), consistent with other trace slice files. -// -// To find the ABI for account A in effect at global_seq Q: -// Binary-search the index for the last entry where account==A && global_seq<=Q, -// then read blob_size bytes from (blob_area_start + blob_offset) in the file. -// --------------------------------------------------------------------------- - -struct abi_store_header { - static constexpr uint32_t magic_value = 0x41424942; // "ABIB" - static constexpr uint32_t current_version = 1; - - uint32_t magic = magic_value; - uint32_t version = current_version; - uint64_t entry_count = 0; -}; -static_assert(sizeof(abi_store_header) == 16); - -struct abi_store_index_entry { - uint64_t account; // chain::name value - uint64_t global_seq; - uint64_t blob_offset; // relative to blob area start - uint64_t blob_size; -}; -static_assert(sizeof(abi_store_index_entry) == 32); - -// --------------------------------------------------------------------------- -// Writer: accumulate (account, global_seq, abi_bytes) triples; write atomically. -// If the same (account, global_seq) key is added twice, last-write-wins. -// --------------------------------------------------------------------------- - -class abi_store_writer { -public: - void add(chain::name account, uint64_t global_seq, std::vector abi_bytes); - void write(const std::filesystem::path& path) const; - // Pre-populate from an existing abi_store.log so previously captured ABIs survive - // across node restarts and are included in the next write(). - void load(const std::filesystem::path& path); - size_t entry_count() const { return _entries.size(); } - -private: - struct entry { - uint64_t account; - uint64_t global_seq; - std::vector abi_bytes; - }; - std::vector _entries; -}; - -// --------------------------------------------------------------------------- -// Reader: load the index from disk, answer (account, global_seq) lookups. -// The file is mmap'd for the lifetime of the reader so that: -// - blob reads are zero-copy memcpy from the page cache (no syscall per query), -// - a concurrent writer's atomic rename swaps the path to a new inode without -// invalidating this reader's view (we still hold the old inode via mmap). -// The index is copied into a vector for binary_search. -// --------------------------------------------------------------------------- - -class abi_store_reader { -public: - explicit abi_store_reader(const std::filesystem::path& path); - - bool valid() const { return _valid; } - - // Returns the ABI bytes in effect for account at global_seq (i.e., the ABI - // with the largest global_seq <= the query), or nullopt if none is found. - std::optional> lookup(chain::name account, uint64_t global_seq) const; - -private: - boost::iostreams::mapped_file_source _file; - std::vector _index; // sorted by (account, global_seq) - uint64_t _blob_area_offset{0}; - bool _valid{false}; -}; - -} // namespace sysio::trace_api - -FC_REFLECT(sysio::trace_api::abi_store_header, (magic)(version)(entry_count)) -FC_REFLECT(sysio::trace_api::abi_store_index_entry, (account)(global_seq)(blob_offset)(blob_size)) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/metadata_log.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/metadata_log.hpp index 21d23d4a54..cc7c60652a 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/metadata_log.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/metadata_log.hpp @@ -7,12 +7,12 @@ namespace sysio { namespace trace_api { struct block_entry_v0 { chain::block_id_type id; - uint32_t number; - uint64_t offset; + uint32_t number = 0; + uint64_t offset = 0; }; struct lib_entry_v0 { - uint32_t lib; + uint32_t lib = 0; }; using metadata_log_entry = std::variant< diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp index ff9011f7a4..daf5b20238 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include #include #include @@ -488,13 +488,9 @@ namespace sysio::trace_api { slice_directory _slice_directory; private: - // ABI sidecar: one global file in the slice directory. - // _abi_write_mutex serialises writes (extraction thread). - // _abi_reader is atomically swapped after each write for lock-free reads (HTTP thread). - mutable std::mutex _abi_write_mutex; - abi_store_writer _abi_writer; - std::atomic> _abi_reader; - std::filesystem::path _abi_store_path; + // ABI sidecar: one global append-only log in the slice directory. + // abi_log serialises its own writes and allows concurrent lookups. + abi_log _abi_log; }; } diff --git a/plugins/trace_api_plugin/src/abi_log.cpp b/plugins/trace_api_plugin/src/abi_log.cpp new file mode 100644 index 0000000000..6fdca0594e --- /dev/null +++ b/plugins/trace_api_plugin/src/abi_log.cpp @@ -0,0 +1,239 @@ +#include + +#include +#include + +#include + +#include +#include +#include + +namespace sysio::trace_api { + +namespace { + +// Record layout: header(24) + blob_bytes(blob_size) + crc32(4). +// CRC covers header + blob_bytes (not itself). +constexpr uint64_t record_trailer_size = sizeof(uint32_t); + +// pread loop that tolerates short reads. Returns true iff `n` bytes were +// read into `buf` from `off`. +bool pread_all(int fd, void* buf, size_t n, uint64_t off) { + auto* p = static_cast(buf); + while (n > 0) { + const ssize_t r = ::pread(fd, p, n, static_cast(off)); + if (r > 0) { + p += r; + off += static_cast(r); + n -= static_cast(r); + } else if (r == 0) { + return false; // EOF before all bytes read + } else if (errno == EINTR) { + continue; + } else { + return false; + } + } + return true; +} + +} // namespace + +uint32_t abi_log::compute_record_crc(const record_header& hdr, const char* blob, uint64_t blob_size) { + boost::crc_32_type crc; + crc.process_bytes(&hdr, sizeof(hdr)); + if (blob_size > 0) + crc.process_bytes(blob, blob_size); + return crc.checksum(); +} + +abi_log::abi_log(const std::filesystem::path& path) { + const bool existed = std::filesystem::exists(path); + _cfile.set_file_path(path); + try { + _cfile.open(fc::cfile::create_or_update_rw_mode); + } catch (...) { + wlog("trace_api: abi_log failed to open {}", path.generic_string()); + return; + } + + if (!existed) { + // Fresh file: write header, no records yet. + abi_log_header hdr; + auto data = fc::raw::pack(hdr); + try { + _cfile.seek(0); + _cfile.write(data.data(), data.size()); + _cfile.flush(); + } catch (...) { + wlog("trace_api: abi_log failed to write header to {}", path.generic_string()); + return; + } + _end_offset = data.size(); + _valid = true; + return; + } + + // Existing file: validate header, scan records, populate index, truncate any bad tail. + try { + _cfile.seek(0); + auto ds = _cfile.create_datastream(); + abi_log_header hdr; + fc::raw::unpack(ds, hdr); + if (hdr.magic != abi_log_header::magic_value) { + wlog("trace_api: abi_log {} has wrong magic {:#x}, ignoring", + path.generic_string(), hdr.magic); + return; + } + if (hdr.version != abi_log_header::current_version) { + wlog("trace_api: abi_log {} has unsupported version {}, ignoring", + path.generic_string(), hdr.version); + return; + } + } catch (...) { + wlog("trace_api: abi_log {} header read failed", path.generic_string()); + return; + } + + _end_offset = recover_from_disk(path); + _valid = true; +} + +uint64_t abi_log::recover_from_disk(const std::filesystem::path& path) { + const uint64_t header_size = sizeof(abi_log_header); + const uint64_t file_size = std::filesystem::file_size(path); + + uint64_t offset = header_size; + while (offset < file_size) { + const uint64_t record_start = offset; + + // Need at least record_header + crc trailer. + if (file_size - record_start < sizeof(record_header) + record_trailer_size) { + wlog("trace_api: abi_log {} torn tail at offset {} (less than minimal record), truncating", + path.generic_string(), record_start); + break; + } + + record_header rh{}; + if (!pread_all(_cfile.fileno(), &rh, sizeof(rh), record_start)) { + wlog("trace_api: abi_log {} failed to read record header at offset {}, truncating", + path.generic_string(), record_start); + break; + } + + const uint64_t blob_offset = record_start + sizeof(record_header); + const uint64_t crc_offset = blob_offset + rh.blob_size; + const uint64_t record_end = crc_offset + record_trailer_size; + + if (record_end > file_size) { + wlog("trace_api: abi_log {} record at {} claims blob_size {} but file has only {} bytes remaining, truncating", + path.generic_string(), record_start, rh.blob_size, file_size - blob_offset); + break; + } + + std::vector blob; + if (rh.blob_size > 0) { + blob.resize(rh.blob_size); + if (!pread_all(_cfile.fileno(), blob.data(), rh.blob_size, blob_offset)) { + wlog("trace_api: abi_log {} failed to read blob at offset {}, truncating", + path.generic_string(), blob_offset); + break; + } + } + + uint32_t stored_crc = 0; + if (!pread_all(_cfile.fileno(), &stored_crc, sizeof(stored_crc), crc_offset)) { + wlog("trace_api: abi_log {} failed to read crc at offset {}, truncating", + path.generic_string(), crc_offset); + break; + } + + const uint32_t computed_crc = compute_record_crc(rh, blob.data(), rh.blob_size); + if (computed_crc != stored_crc) { + wlog("trace_api: abi_log {} crc mismatch at record offset {} (stored {:#x} vs computed {:#x}), truncating", + path.generic_string(), record_start, stored_crc, computed_crc); + break; + } + + _index[{rh.account, rh.global_seq}] = index_entry{blob_offset, rh.blob_size}; + offset = record_end; + } + + if (offset < file_size) { + // Drop the bad tail. resize_file works regardless of whether the + // file is currently open; subsequent writes via _cfile will append + // from _end_offset. + std::error_code ec; + std::filesystem::resize_file(path, offset, ec); + if (ec) { + wlog("trace_api: abi_log {} failed to truncate to {}: {}", + path.generic_string(), offset, ec.message()); + } + } + + return offset; +} + +void abi_log::append(chain::name account, uint64_t global_seq, std::vector abi_bytes) { + if (!_valid) return; + + record_header rh{ account.to_uint64_t(), global_seq, abi_bytes.size() }; + const uint32_t crc = compute_record_crc(rh, abi_bytes.data(), abi_bytes.size()); + + uint64_t blob_offset = 0; + { + std::lock_guard lock(_append_mtx); + try { + _cfile.seek(_end_offset); + _cfile.write(reinterpret_cast(&rh), sizeof(rh)); + if (rh.blob_size > 0) + _cfile.write(abi_bytes.data(), abi_bytes.size()); + _cfile.write(reinterpret_cast(&crc), sizeof(crc)); + _cfile.flush(); + } catch (...) { + wlog("trace_api: abi_log append failed at offset {}", _end_offset); + return; + } + + blob_offset = _end_offset + sizeof(rh); + _end_offset += sizeof(rh) + rh.blob_size + record_trailer_size; + } + + { + std::lock_guard lock(_index_mtx); + _index[{rh.account, rh.global_seq}] = index_entry{blob_offset, rh.blob_size}; + } +} + +std::optional> abi_log::lookup(chain::name account, uint64_t global_seq) const { + if (!_valid) return std::nullopt; + + const uint64_t acct = account.to_uint64_t(); + uint64_t blob_offset = 0; + uint64_t blob_size = 0; + + { + std::lock_guard lock(_index_mtx); + auto it = _index.upper_bound({acct, global_seq}); + if (it == _index.begin()) + return std::nullopt; + --it; + if (it->first.first != acct) + return std::nullopt; + blob_offset = it->second.blob_offset; + blob_size = it->second.blob_size; + } + + if (blob_size == 0) + return std::vector{}; + + std::vector out(blob_size); + if (!pread_all(_cfile.fileno(), out.data(), blob_size, blob_offset)) { + wlog("trace_api: abi_log pread of {} bytes at {} failed", blob_size, blob_offset); + return std::nullopt; + } + return out; +} + +} // namespace sysio::trace_api diff --git a/plugins/trace_api_plugin/src/abi_store.cpp b/plugins/trace_api_plugin/src/abi_store.cpp deleted file mode 100644 index c8f1bb513d..0000000000 --- a/plugins/trace_api_plugin/src/abi_store.cpp +++ /dev/null @@ -1,201 +0,0 @@ -#include - -#include -#include - -#include -#include - -namespace sysio::trace_api { - -// --------------------------------------------------------------------------- -// abi_store_writer -// --------------------------------------------------------------------------- - -void abi_store_writer::add(chain::name account, uint64_t global_seq, std::vector abi_bytes) { - _entries.push_back({account.to_uint64_t(), global_seq, std::move(abi_bytes)}); -} - -void abi_store_writer::write(const std::filesystem::path& path) const { - // Sort by (account, global_seq); stable so last-add-wins for duplicate keys. - std::vector order(_entries.size()); - std::iota(order.begin(), order.end(), 0); - std::stable_sort(order.begin(), order.end(), [&](size_t a, size_t b) { - if (_entries[a].account != _entries[b].account) - return _entries[a].account < _entries[b].account; - return _entries[a].global_seq < _entries[b].global_seq; - }); - - // Compute blob offsets (relative to blob area start). - uint64_t running_offset = 0; - std::vector blob_offsets(order.size()); - for (size_t i = 0; i < order.size(); ++i) { - blob_offsets[i] = running_offset; - running_offset += _entries[order[i]].abi_bytes.size(); - } - - const auto tmp_path = std::filesystem::path(path).replace_extension(".tmp"); - - fc::cfile f; - f.set_file_path(tmp_path); - f.open(fc::cfile::create_or_update_rw_mode); - - // Header - abi_store_header hdr; - hdr.entry_count = order.size(); - auto hdr_data = fc::raw::pack(hdr); - f.write(hdr_data.data(), hdr_data.size()); - - // Index (sorted) - for (size_t i = 0; i < order.size(); ++i) { - const auto& e = _entries[order[i]]; - abi_store_index_entry ie; - ie.account = e.account; - ie.global_seq = e.global_seq; - ie.blob_offset = blob_offsets[i]; - ie.blob_size = e.abi_bytes.size(); - auto ie_data = fc::raw::pack(ie); - f.write(ie_data.data(), ie_data.size()); - } - - // Blob area (in same sorted order) - for (size_t i : order) { - const auto& blob = _entries[i].abi_bytes; - if (!blob.empty()) - f.write(blob.data(), blob.size()); - } - - f.flush(); - f.close(); - - std::filesystem::rename(tmp_path, path); -} - -// --------------------------------------------------------------------------- -// abi_store_reader -// --------------------------------------------------------------------------- - -abi_store_reader::abi_store_reader(const std::filesystem::path& path) { - if (!std::filesystem::exists(path)) - return; - - try { - _file.open(path.string()); - } catch (...) { - wlog("trace_api: failed to mmap abi_store from {}", path.generic_string()); - return; - } - if (!_file.is_open() || _file.size() < sizeof(abi_store_header)) { - wlog("trace_api: abi_store {} too small for header, ignoring", path.generic_string()); - return; - } - - // fc::raw::pack uses little-endian for fixed-size integers and the structs - // are packed plain (no padding). On x86_64 (little-endian native) the on-disk - // bytes match the in-memory struct layout, so reinterpret_cast is safe. - // The trace_api_plugin is x86_64 Linux only; documented in the slice-file - // header comments throughout this directory. - const char* base = _file.data(); - abi_store_header hdr; - std::memcpy(&hdr, base, sizeof(hdr)); - - if (hdr.magic != abi_store_header::magic_value) { - wlog("trace_api: abi_store {} has wrong magic, ignoring", path.generic_string()); - return; - } - if (hdr.version != abi_store_header::current_version) { - wlog("trace_api: abi_store {} has unsupported version {}, ignoring", - path.generic_string(), hdr.version); - return; - } - - const uint64_t expected_min_size = sizeof(abi_store_header) + - static_cast(hdr.entry_count) * sizeof(abi_store_index_entry); - if (_file.size() < expected_min_size) { - wlog("trace_api: abi_store {} truncated (size {} < expected min {}), ignoring", - path.generic_string(), _file.size(), expected_min_size); - return; - } - - _index.resize(hdr.entry_count); - if (hdr.entry_count > 0) { - std::memcpy(_index.data(), - base + sizeof(abi_store_header), - static_cast(hdr.entry_count) * sizeof(abi_store_index_entry)); - } - - _blob_area_offset = expected_min_size; - _valid = true; -} - -std::optional> abi_store_reader::lookup(chain::name account, uint64_t global_seq) const { - if (!_valid || _index.empty()) - return std::nullopt; - - const uint64_t acct = account.to_uint64_t(); - - // upper_bound gives first entry strictly greater than (acct, global_seq). - // Decrement to get the last entry <= the query, then check the account matches. - const abi_store_index_entry key{acct, global_seq, 0, 0}; - auto it = std::upper_bound(_index.begin(), _index.end(), key, - [](const abi_store_index_entry& a, const abi_store_index_entry& b) { - if (a.account != b.account) return a.account < b.account; - return a.global_seq < b.global_seq; - }); - - if (it == _index.begin()) - return std::nullopt; - --it; - if (it->account != acct) - return std::nullopt; - - if (it->blob_size == 0) - return std::vector{}; - - const uint64_t blob_start = _blob_area_offset + it->blob_offset; - if (blob_start + it->blob_size > _file.size()) { - // Truncated or corrupt index — refuse rather than read past the mapping. - return std::nullopt; - } - const char* blob_ptr = _file.data() + blob_start; - return std::vector(blob_ptr, blob_ptr + it->blob_size); -} - -// --------------------------------------------------------------------------- -// abi_store_writer::load -// --------------------------------------------------------------------------- - -void abi_store_writer::load(const std::filesystem::path& path) { - try { - fc::cfile f; - f.set_file_path(path); - f.open("rb"); - f.seek(0); - auto ds = f.create_datastream(); - - abi_store_header hdr; - fc::raw::unpack(ds, hdr); - if (hdr.magic != abi_store_header::magic_value) return; - if (hdr.version != abi_store_header::current_version) return; - - std::vector index(hdr.entry_count); - for (auto& e : index) - fc::raw::unpack(ds, e); - - const uint64_t blob_area_start = sizeof(abi_store_header) + - static_cast(hdr.entry_count) * sizeof(abi_store_index_entry); - for (const auto& ie : index) { - std::vector blob(ie.blob_size); - if (ie.blob_size > 0) { - f.seek(static_cast(blob_area_start + ie.blob_offset)); - f.read(blob.data(), ie.blob_size); - } - _entries.push_back({ie.account, ie.global_seq, std::move(blob)}); - } - } catch (...) { - wlog("trace_api: failed to load abi_store into writer from {}", path.generic_string()); - _entries.clear(); - } -} - -} // namespace sysio::trace_api diff --git a/plugins/trace_api_plugin/src/store_provider.cpp b/plugins/trace_api_plugin/src/store_provider.cpp index 1acf3096b2..5e71dd00b4 100644 --- a/plugins/trace_api_plugin/src/store_provider.cpp +++ b/plugins/trace_api_plugin/src/store_provider.cpp @@ -37,12 +37,7 @@ namespace sysio::trace_api { store_provider::store_provider(const std::filesystem::path& slice_dir, uint32_t stride_width, std::optional minimum_irreversible_history_blocks, std::optional minimum_uncompressed_irreversible_history_blocks, size_t compression_seek_point_stride) : _slice_directory(slice_dir, stride_width, minimum_irreversible_history_blocks, minimum_uncompressed_irreversible_history_blocks, compression_seek_point_stride) - , _abi_store_path(slice_dir / "abi_store.log") { - if (std::filesystem::exists(_abi_store_path)) { - _abi_writer.load(_abi_store_path); - _abi_reader.store(std::make_shared(_abi_store_path)); - } - } + , _abi_log(slice_dir / "abi_log.log") {} template void store_provider::append(const BlockTrace& bt) { @@ -414,16 +409,11 @@ namespace sysio::trace_api { } void store_provider::append_abi(chain::name account, uint64_t global_seq, std::vector abi_bytes) { - std::lock_guard lock(_abi_write_mutex); - _abi_writer.add(account, global_seq, std::move(abi_bytes)); - _abi_writer.write(_abi_store_path); - _abi_reader.store(std::make_shared(_abi_store_path)); + _abi_log.append(account, global_seq, std::move(abi_bytes)); } std::optional> store_provider::lookup_abi(chain::name account, uint64_t global_seq) const { - auto reader = _abi_reader.load(); - if (!reader) return std::nullopt; - return reader->lookup(account, global_seq); + return _abi_log.lookup(account, global_seq); } uint32_t slice_directory::slice_number_from_path(const std::filesystem::path& trx_id_path) const { diff --git a/plugins/trace_api_plugin/test/CMakeLists.txt b/plugins/trace_api_plugin/test/CMakeLists.txt index f11e5a046b..f5615fef49 100644 --- a/plugins/trace_api_plugin/test/CMakeLists.txt +++ b/plugins/trace_api_plugin/test/CMakeLists.txt @@ -7,7 +7,7 @@ add_executable( test_trace_api_plugin test_configuration_utils.cpp test_compressed_file.cpp test_trx_id_index.cpp - test_abi_store.cpp + test_abi_log.cpp test_get_actions.cpp main.cpp ) diff --git a/plugins/trace_api_plugin/test/test_abi_log.cpp b/plugins/trace_api_plugin/test/test_abi_log.cpp new file mode 100644 index 0000000000..c38594cf95 --- /dev/null +++ b/plugins/trace_api_plugin/test/test_abi_log.cpp @@ -0,0 +1,313 @@ +#include +#include +#include + +#include +#include + +#include +#include + +using namespace sysio; +using namespace sysio::trace_api; +using namespace sysio::trace_api::test_common; + +namespace { + +struct abi_log_fixture { + fc::temp_directory tempdir; + + std::filesystem::path log_path() const { + return tempdir.path() / "abi_log.log"; + } + + static std::vector make_abi(const std::string& tag) { + return std::vector(tag.begin(), tag.end()); + } + + // Open a log, run callable with it, then let it destruct (closes the file). + template + void with_log(F&& f) { + abi_log log(log_path()); + BOOST_REQUIRE(log.valid()); + f(log); + } +}; + +} // namespace + +BOOST_AUTO_TEST_SUITE(abi_log_tests) + +// --------------------------------------------------------------------------- +// Round-trip: writer/reader in the same process +// --------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE(empty_log_is_valid, abi_log_fixture) { + abi_log log(log_path()); + BOOST_CHECK(log.valid()); + BOOST_CHECK(!log.lookup("sysio.token"_n, 1)); + BOOST_CHECK(!log.lookup("sysio.token"_n, 0)); +} + +BOOST_FIXTURE_TEST_CASE(single_entry_round_trip, abi_log_fixture) { + abi_log log(log_path()); + auto blob = make_abi("abi-v1"); + log.append("sysio.token"_n, 100, blob); + + auto result = log.lookup("sysio.token"_n, 100); + BOOST_REQUIRE(result.has_value()); + BOOST_CHECK_EQUAL_COLLECTIONS(result->begin(), result->end(), blob.begin(), blob.end()); +} + +BOOST_FIXTURE_TEST_CASE(multiple_accounts_round_trip, abi_log_fixture) { + abi_log log(log_path()); + auto blob1 = make_abi("token-abi-v1"); + auto blob2 = make_abi("eosio-abi-v1"); + log.append("sysio.token"_n, 50, blob1); + log.append("sysio"_n, 200, blob2); + + auto r1 = log.lookup("sysio.token"_n, 50); + BOOST_REQUIRE(r1.has_value()); + BOOST_CHECK_EQUAL_COLLECTIONS(r1->begin(), r1->end(), blob1.begin(), blob1.end()); + + auto r2 = log.lookup("sysio"_n, 200); + BOOST_REQUIRE(r2.has_value()); + BOOST_CHECK_EQUAL_COLLECTIONS(r2->begin(), r2->end(), blob2.begin(), blob2.end()); +} + +// --------------------------------------------------------------------------- +// Version selection via upper_bound +// --------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE(returns_abi_in_effect_at_global_seq, abi_log_fixture) { + abi_log log(log_path()); + auto v1 = make_abi("abi-v1"); + auto v2 = make_abi("abi-v2"); + auto v3 = make_abi("abi-v3"); + + log.append("sysio.token"_n, 100, v1); + log.append("sysio.token"_n, 200, v2); + log.append("sysio.token"_n, 300, v3); + + // Before any version + BOOST_CHECK(!log.lookup("sysio.token"_n, 99)); + + // Exactly at v1 + auto at100 = log.lookup("sysio.token"_n, 100); + BOOST_REQUIRE(at100.has_value()); + BOOST_CHECK_EQUAL_COLLECTIONS(at100->begin(), at100->end(), v1.begin(), v1.end()); + + // Between v1 and v2 -> v1 + auto at150 = log.lookup("sysio.token"_n, 150); + BOOST_REQUIRE(at150.has_value()); + BOOST_CHECK_EQUAL_COLLECTIONS(at150->begin(), at150->end(), v1.begin(), v1.end()); + + // Exactly at v2 + auto at200 = log.lookup("sysio.token"_n, 200); + BOOST_REQUIRE(at200.has_value()); + BOOST_CHECK_EQUAL_COLLECTIONS(at200->begin(), at200->end(), v2.begin(), v2.end()); + + // After v3 + auto at500 = log.lookup("sysio.token"_n, 500); + BOOST_REQUIRE(at500.has_value()); + BOOST_CHECK_EQUAL_COLLECTIONS(at500->begin(), at500->end(), v3.begin(), v3.end()); +} + +BOOST_FIXTURE_TEST_CASE(lookup_wrong_account_returns_nullopt, abi_log_fixture) { + abi_log log(log_path()); + log.append("sysio.token"_n, 100, make_abi("token-abi")); + BOOST_CHECK(!log.lookup("sysio.msig"_n, 100)); +} + +BOOST_FIXTURE_TEST_CASE(accounts_do_not_bleed_into_each_other, abi_log_fixture) { + abi_log log(log_path()); + log.append("sysio.token"_n, 100, make_abi("token-100")); + log.append("sysio"_n, 200, make_abi("sysio-200")); + + auto token_result = log.lookup("sysio.token"_n, 250); + BOOST_REQUIRE(token_result.has_value()); + BOOST_CHECK(*token_result == make_abi("token-100")); + + BOOST_CHECK(!log.lookup("sysio.token"_n, 50)); +} + +BOOST_FIXTURE_TEST_CASE(empty_blob_round_trip, abi_log_fixture) { + abi_log log(log_path()); + log.append("clearme"_n, 999, {}); // empty ABI + + auto result = log.lookup("clearme"_n, 999); + BOOST_REQUIRE(result.has_value()); + BOOST_CHECK(result->empty()); +} + +BOOST_FIXTURE_TEST_CASE(last_write_wins_for_duplicate_key, abi_log_fixture) { + abi_log log(log_path()); + log.append("acct"_n, 100, make_abi("first")); + log.append("acct"_n, 100, make_abi("second")); // overwrites in index + + auto result = log.lookup("acct"_n, 100); + BOOST_REQUIRE(result.has_value()); + BOOST_CHECK(*result == make_abi("second")); +} + +// --------------------------------------------------------------------------- +// Restart: entries persist across open/close cycles +// --------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE(append_after_restart, abi_log_fixture) { + // Session 1: write two records, close. + { + abi_log log(log_path()); + log.append("sysio.token"_n, 100, make_abi("token-100")); + log.append("sysio"_n, 200, make_abi("sysio-200")); + } + + // Session 2: reopen, verify old records, append a new one, verify all three. + abi_log log(log_path()); + BOOST_REQUIRE(log.valid()); + + auto r1 = log.lookup("sysio.token"_n, 100); + BOOST_REQUIRE(r1.has_value()); + BOOST_CHECK(*r1 == make_abi("token-100")); + + auto r2 = log.lookup("sysio"_n, 200); + BOOST_REQUIRE(r2.has_value()); + BOOST_CHECK(*r2 == make_abi("sysio-200")); + + log.append("newacct"_n, 300, make_abi("new-300")); + + auto r3 = log.lookup("newacct"_n, 300); + BOOST_REQUIRE(r3.has_value()); + BOOST_CHECK(*r3 == make_abi("new-300")); +} + +// --------------------------------------------------------------------------- +// Error paths: bad header +// --------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE(bad_magic_is_invalid, abi_log_fixture) { + { + std::ofstream f(log_path(), std::ios::binary); + abi_log_header hdr; + hdr.magic = 0xDEADBEEF; + f.write(reinterpret_cast(&hdr), sizeof(hdr)); + } + abi_log log(log_path()); + BOOST_CHECK(!log.valid()); +} + +BOOST_FIXTURE_TEST_CASE(bad_version_is_invalid, abi_log_fixture) { + { + std::ofstream f(log_path(), std::ios::binary); + abi_log_header hdr; + hdr.version = 99; + f.write(reinterpret_cast(&hdr), sizeof(hdr)); + } + abi_log log(log_path()); + BOOST_CHECK(!log.valid()); +} + +// --------------------------------------------------------------------------- +// Corruption recovery: torn tail +// --------------------------------------------------------------------------- + +// Chop bytes off the end of the file. The final record's trailing CRC is +// truncated, so recovery drops it at reopen. +BOOST_FIXTURE_TEST_CASE(truncated_tail_is_recovered, abi_log_fixture) { + // Session 1: write three records. + { + abi_log log(log_path()); + log.append("a"_n, 100, make_abi("a-100")); + log.append("b"_n, 200, make_abi("b-200")); + log.append("c"_n, 300, make_abi("c-300")); + } + + // Lop off the last 3 bytes (inside record-c's crc). + { + const auto size = std::filesystem::file_size(log_path()); + std::filesystem::resize_file(log_path(), size - 3); + } + + // Session 2: recover. a and b survive, c is gone. + abi_log log(log_path()); + BOOST_REQUIRE(log.valid()); + BOOST_CHECK(log.lookup("a"_n, 100).has_value()); + BOOST_CHECK(log.lookup("b"_n, 200).has_value()); + BOOST_CHECK(!log.lookup("c"_n, 300)); + + // File should have been truncated at end of record-b, so a new append + // lands at that position and is recoverable. + log.append("d"_n, 400, make_abi("d-400")); + auto r = log.lookup("d"_n, 400); + BOOST_REQUIRE(r.has_value()); + BOOST_CHECK(*r == make_abi("d-400")); +} + +// Flip a byte inside record-b's blob so its CRC fails. Recovery truncates +// everything from the start of record-b onwards (including record-c). +BOOST_FIXTURE_TEST_CASE(crc_mismatch_drops_record_and_tail, abi_log_fixture) { + { + abi_log log(log_path()); + log.append("a"_n, 100, make_abi("a-blob")); + log.append("b"_n, 200, make_abi("b-blob")); + log.append("c"_n, 300, make_abi("c-blob")); + } + + // Layout per record: header(24) + blob + crc(4). + // Record a: offset 16 (header), header end 40, blob 40..46, crc 46..50 + // Record b: offset 50 (header), header end 74, blob 74..80, crc 80..84 + // Flip byte at offset 75 (middle of b's blob). + { + std::fstream f(log_path(), std::ios::binary | std::ios::in | std::ios::out); + f.seekp(75); + char c = 0; + f.read(&c, 1); + c ^= 0xff; + f.seekp(75); + f.write(&c, 1); + } + + abi_log log(log_path()); + BOOST_REQUIRE(log.valid()); + BOOST_CHECK(log.lookup("a"_n, 100).has_value()); + BOOST_CHECK(!log.lookup("b"_n, 200)); + BOOST_CHECK(!log.lookup("c"_n, 300)); + + // File truncated at end of record-a (offset 50). New append works. + log.append("d"_n, 400, make_abi("d-blob")); + auto r = log.lookup("d"_n, 400); + BOOST_REQUIRE(r.has_value()); + BOOST_CHECK(*r == make_abi("d-blob")); +} + +// --------------------------------------------------------------------------- +// Many records +// --------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE(many_accounts_many_versions, abi_log_fixture) { + const int NUM_ACCOUNTS = 10; + const int VERSIONS_PER_ACCOUNT = 5; + + abi_log log(log_path()); + + // Add in reverse order to verify the in-memory index sort is correct. + for (int a = NUM_ACCOUNTS - 1; a >= 0; --a) { + for (int v = VERSIONS_PER_ACCOUNT - 1; v >= 0; --v) { + auto acct = chain::name(static_cast(a + 1) * 0x10000000000000ULL); + uint64_t seq = static_cast(a * 100 + v * 10 + 1); + log.append(acct, seq, make_abi("a" + std::to_string(a) + "v" + std::to_string(v))); + } + } + + for (int a = 0; a < NUM_ACCOUNTS; ++a) { + auto acct = chain::name(static_cast(a + 1) * 0x10000000000000ULL); + int v = VERSIONS_PER_ACCOUNT - 1; + uint64_t seq = static_cast(a * 100 + v * 10 + 1); + auto result = log.lookup(acct, seq); + BOOST_REQUIRE_MESSAGE(result.has_value(), "missing entry for account " << a << " version " << v); + auto expected = make_abi("a" + std::to_string(a) + "v" + std::to_string(v)); + BOOST_CHECK_EQUAL_COLLECTIONS(result->begin(), result->end(), expected.begin(), expected.end()); + } +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/plugins/trace_api_plugin/test/test_abi_store.cpp b/plugins/trace_api_plugin/test/test_abi_store.cpp deleted file mode 100644 index ac70bef52a..0000000000 --- a/plugins/trace_api_plugin/test/test_abi_store.cpp +++ /dev/null @@ -1,247 +0,0 @@ -#include -#include -#include - -#include -#include - -using namespace sysio; -using namespace sysio::trace_api; -using namespace sysio::trace_api::test_common; - -namespace { - -struct abi_store_fixture { - fc::temp_directory tempdir; - - std::filesystem::path store_path() const { - return tempdir.path() / "test_abi_store.log"; - } - - // Convenience: make a small ABI blob from a string tag - static std::vector make_abi(const std::string& tag) { - return std::vector(tag.begin(), tag.end()); - } -}; - -} // namespace - -BOOST_AUTO_TEST_SUITE(abi_store_tests) - -// --------------------------------------------------------------------------- -// Writer / Reader round-trip -// --------------------------------------------------------------------------- - -BOOST_FIXTURE_TEST_CASE(empty_writer_is_valid, abi_store_fixture) { - abi_store_writer w; - BOOST_REQUIRE_EQUAL(w.entry_count(), 0u); - w.write(store_path()); - - abi_store_reader r(store_path()); - BOOST_CHECK(r.valid()); - BOOST_CHECK(!r.lookup("sysio.token"_n, 1)); -} - -BOOST_FIXTURE_TEST_CASE(single_entry_round_trip, abi_store_fixture) { - auto blob = make_abi("abi-v1"); - abi_store_writer w; - w.add("sysio.token"_n, 100, blob); - w.write(store_path()); - - abi_store_reader r(store_path()); - BOOST_REQUIRE(r.valid()); - - auto result = r.lookup("sysio.token"_n, 100); - BOOST_REQUIRE(result.has_value()); - BOOST_CHECK_EQUAL_COLLECTIONS(result->begin(), result->end(), blob.begin(), blob.end()); -} - -BOOST_FIXTURE_TEST_CASE(multiple_accounts_round_trip, abi_store_fixture) { - auto blob1 = make_abi("token-abi-v1"); - auto blob2 = make_abi("eosio-abi-v1"); - abi_store_writer w; - w.add("sysio.token"_n, 50, blob1); - w.add("sysio"_n, 200, blob2); - w.write(store_path()); - - abi_store_reader r(store_path()); - BOOST_REQUIRE(r.valid()); - - auto r1 = r.lookup("sysio.token"_n, 50); - BOOST_REQUIRE(r1.has_value()); - BOOST_CHECK_EQUAL_COLLECTIONS(r1->begin(), r1->end(), blob1.begin(), blob1.end()); - - auto r2 = r.lookup("sysio"_n, 200); - BOOST_REQUIRE(r2.has_value()); - BOOST_CHECK_EQUAL_COLLECTIONS(r2->begin(), r2->end(), blob2.begin(), blob2.end()); -} - -// --------------------------------------------------------------------------- -// ABI version selection (return the version in effect at query global_seq) -// --------------------------------------------------------------------------- - -BOOST_FIXTURE_TEST_CASE(returns_abi_in_effect_at_global_seq, abi_store_fixture) { - // Three ABI versions for the same account - auto v1 = make_abi("abi-v1"); - auto v2 = make_abi("abi-v2"); - auto v3 = make_abi("abi-v3"); - - abi_store_writer w; - w.add("sysio.token"_n, 100, v1); - w.add("sysio.token"_n, 200, v2); - w.add("sysio.token"_n, 300, v3); - w.write(store_path()); - - abi_store_reader r(store_path()); - BOOST_REQUIRE(r.valid()); - - // Before any version: not found - BOOST_CHECK(!r.lookup("sysio.token"_n, 99)); - - // Exactly at v1 - auto at100 = r.lookup("sysio.token"_n, 100); - BOOST_REQUIRE(at100.has_value()); - BOOST_CHECK_EQUAL_COLLECTIONS(at100->begin(), at100->end(), v1.begin(), v1.end()); - - // Between v1 and v2: v1 is in effect - auto at150 = r.lookup("sysio.token"_n, 150); - BOOST_REQUIRE(at150.has_value()); - BOOST_CHECK_EQUAL_COLLECTIONS(at150->begin(), at150->end(), v1.begin(), v1.end()); - - // Exactly at v2 - auto at200 = r.lookup("sysio.token"_n, 200); - BOOST_REQUIRE(at200.has_value()); - BOOST_CHECK_EQUAL_COLLECTIONS(at200->begin(), at200->end(), v2.begin(), v2.end()); - - // After v3 - auto at500 = r.lookup("sysio.token"_n, 500); - BOOST_REQUIRE(at500.has_value()); - BOOST_CHECK_EQUAL_COLLECTIONS(at500->begin(), at500->end(), v3.begin(), v3.end()); -} - -BOOST_FIXTURE_TEST_CASE(lookup_wrong_account_returns_nullopt, abi_store_fixture) { - abi_store_writer w; - w.add("sysio.token"_n, 100, make_abi("token-abi")); - w.write(store_path()); - - abi_store_reader r(store_path()); - BOOST_REQUIRE(r.valid()); - BOOST_CHECK(!r.lookup("sysio.msig"_n, 100)); // different account -} - -// --------------------------------------------------------------------------- -// Multiple accounts share the sorted index correctly -// --------------------------------------------------------------------------- - -BOOST_FIXTURE_TEST_CASE(accounts_do_not_bleed_into_each_other, abi_store_fixture) { - // sysio.token has v1 at seq=100; sysio has v1 at seq=200. - // Looking up sysio.token at seq=250 must return sysio.token's ABI, not sysio's. - abi_store_writer w; - w.add("sysio.token"_n, 100, make_abi("token-100")); - w.add("sysio"_n, 200, make_abi("sysio-200")); - w.write(store_path()); - - abi_store_reader r(store_path()); - BOOST_REQUIRE(r.valid()); - - auto token_result = r.lookup("sysio.token"_n, 250); - BOOST_REQUIRE(token_result.has_value()); - BOOST_CHECK(*token_result == make_abi("token-100")); - - // sysio.token at seq=50: not found (before first version) - BOOST_CHECK(!r.lookup("sysio.token"_n, 50)); -} - -// --------------------------------------------------------------------------- -// Empty ABI blob (account deleted / cleared ABI) -// --------------------------------------------------------------------------- - -BOOST_FIXTURE_TEST_CASE(empty_blob_round_trip, abi_store_fixture) { - abi_store_writer w; - w.add("clearme"_n, 999, {}); // empty ABI - w.write(store_path()); - - abi_store_reader r(store_path()); - BOOST_REQUIRE(r.valid()); - - auto result = r.lookup("clearme"_n, 999); - BOOST_REQUIRE(result.has_value()); - BOOST_CHECK(result->empty()); -} - -// --------------------------------------------------------------------------- -// Error paths -// --------------------------------------------------------------------------- - -BOOST_FIXTURE_TEST_CASE(missing_file_is_invalid, abi_store_fixture) { - abi_store_reader r(tempdir.path() / "nonexistent.log"); - BOOST_CHECK(!r.valid()); -} - -BOOST_FIXTURE_TEST_CASE(bad_magic_is_invalid, abi_store_fixture) { - fc::cfile f; - f.set_file_path(store_path()); - f.open(fc::cfile::create_or_update_rw_mode); - abi_store_header hdr; - hdr.magic = 0xDEADBEEF; - auto data = fc::raw::pack(hdr); - f.write(data.data(), data.size()); - f.flush(); - f.close(); - - abi_store_reader r(store_path()); - BOOST_CHECK(!r.valid()); -} - -BOOST_FIXTURE_TEST_CASE(bad_version_is_invalid, abi_store_fixture) { - fc::cfile f; - f.set_file_path(store_path()); - f.open(fc::cfile::create_or_update_rw_mode); - abi_store_header hdr; - hdr.version = 99; - auto data = fc::raw::pack(hdr); - f.write(data.data(), data.size()); - f.flush(); - f.close(); - - abi_store_reader r(store_path()); - BOOST_CHECK(!r.valid()); -} - -// --------------------------------------------------------------------------- -// Many entries -// --------------------------------------------------------------------------- - -BOOST_FIXTURE_TEST_CASE(many_accounts_many_versions, abi_store_fixture) { - const int NUM_ACCOUNTS = 10; - const int VERSIONS_PER_ACCOUNT = 5; - - abi_store_writer w; - // Add in reverse order to verify the writer sorts correctly. - for (int a = NUM_ACCOUNTS - 1; a >= 0; --a) { - for (int v = VERSIONS_PER_ACCOUNT - 1; v >= 0; --v) { - auto acct = chain::name(static_cast(a + 1) * 0x10000000000000ULL); - uint64_t seq = static_cast(a * 100 + v * 10 + 1); - w.add(acct, seq, make_abi("a" + std::to_string(a) + "v" + std::to_string(v))); - } - } - BOOST_REQUIRE_EQUAL(w.entry_count(), static_cast(NUM_ACCOUNTS * VERSIONS_PER_ACCOUNT)); - w.write(store_path()); - - abi_store_reader r(store_path()); - BOOST_REQUIRE(r.valid()); - - // Spot-check several (account, global_seq) lookups - for (int a = 0; a < NUM_ACCOUNTS; ++a) { - auto acct = chain::name(static_cast(a + 1) * 0x10000000000000ULL); - // Lookup at the exact seq of the last version for this account - int v = VERSIONS_PER_ACCOUNT - 1; - uint64_t seq = static_cast(a * 100 + v * 10 + 1); - auto result = r.lookup(acct, seq); - BOOST_REQUIRE_MESSAGE(result.has_value(), "missing entry for account " << a << " version " << v); - auto expected = make_abi("a" + std::to_string(a) + "v" + std::to_string(v)); - BOOST_CHECK_EQUAL_COLLECTIONS(result->begin(), result->end(), expected.begin(), expected.end()); - } -} - -BOOST_AUTO_TEST_SUITE_END() diff --git a/plugins/trace_api_plugin/test/test_extraction.cpp b/plugins/trace_api_plugin/test/test_extraction.cpp index fc66c54fae..9521cd8f84 100644 --- a/plugins/trace_api_plugin/test/test_extraction.cpp +++ b/plugins/trace_api_plugin/test/test_extraction.cpp @@ -114,7 +114,7 @@ struct extraction_test_fixture { } void append_abi(chain::name, uint64_t, std::vector) { - // not tested here; abi_store tests cover this + // not tested here; abi_log tests cover this } extraction_test_fixture& fixture; diff --git a/plugins/trace_api_plugin/trace_api_plugin.md b/plugins/trace_api_plugin/trace_api_plugin.md index 54d11067df..542528da56 100644 --- a/plugins/trace_api_plugin/trace_api_plugin.md +++ b/plugins/trace_api_plugin/trace_api_plugin.md @@ -46,7 +46,7 @@ Key design points: action_traces` is stored, so inline notifications are captured alongside the originating action. - **Versioned ABI decoding** — the ABI in effect at the moment each - `setcode`/`setabi` transaction was applied is captured in `abi_store.log`. + `setcode`/`setabi` transaction was applied is captured in `abi_log.log`. Queries decode `data` and `return_value` fields using the historically correct ABI, not the current on-chain ABI. - **O(1) transaction lookup** — a per-slice hash index maps `trx_id` to @@ -142,7 +142,7 @@ traces/ trace_index_0000020000-0000030000.log trace_blk_idx_0000020000-0000030000.log trace_trx_idx_0000020000-0000030000.log - abi_store.log + abi_log.log ``` ### Block-offset index @@ -176,28 +176,35 @@ irreversible. Queries against `/v1/trace_api/get_transaction_trace` use this index for O(1) `trx_id → block_num` resolution instead of scanning the chain. -### ABI store +### ABI log -`abi_store.log` is a single file that persists the ABI published by each +`abi_log.log` is an append-only file that persists the ABI published by each contract account across all `setabi` transactions observed since the node started (or since the file was first written). Format: ``` -Header (16 bytes): magic "ABIB" (u32), version 1 (u32), entry_count (u64) -Index (entry_count × 32 bytes, sorted account ASC, global_seq ASC): - account(u64) | global_seq(u64) | blob_offset(u64) | blob_size(u64) -Blob area (variable): raw fc::raw-packed abi_def bytes in index order +Header (16 bytes): magic "ABIL" (u32), version 1 (u32), reserved (u64) +Records (repeated until EOF): + account (u64) + global_seq (u64) + blob_size (u64) + blob_bytes (blob_size bytes) + crc32 (u32) over (account, global_seq, blob_size, blob_bytes) ``` -To find the ABI for contract `A` in effect at `global_seq Q`: binary-search -the index for the last entry where `account == A && global_seq <= Q`, then -read the referenced blob. +An in-memory index keyed by `(account, global_sequence)` is built at startup +by walking the file record-by-record and validating each CRC. Runtime +lookups go through the index; the matching blob is then read from the file +via `pread()`. Appends stream new records to the end of the file under a +mutex, with no rewrite of existing records. -The file is written atomically (write to `.tmp`, then rename) after each -block. On node restart it is loaded into the writer so previously captured -ABIs survive across restarts. +Writes are not fsync'd; the on-disk tail may lose the last few records on a +kernel crash. On startup the recovery scan detects torn or CRC-mismatched +records and truncates the file at the first bad one — any lost records are +rebuilt the next time their contract is touched (via observed `setabi` or +the lazy current-ABI fetch). --- @@ -222,7 +229,7 @@ A continuity gap does not prevent the node from running, but `get_block` and When serving any trace endpoint the plugin attempts to decode the raw `data` and `return_value` bytes of each action using the ABI captured in -`abi_store.log`. +`abi_log.log`. - The lookup key is `(account, global_sequence)` — the ABI that was in effect when that specific action executed is used, not the current ABI. @@ -696,11 +703,12 @@ continues. If the gap is large, consider either: 2. Deleting the trace directory entirely and re-syncing from genesis (only practical for newer chains). -### abi_store.log +### abi_log.log -`abi_store.log` is rewritten after every block. If it is lost or corrupted, -delete it and restart nodeop. The plugin will rebuild it as new `setabi` -transactions are applied; historical ABI lookup for events before the loss +`abi_log.log` is append-only. If it is lost or corrupted, delete it and +restart nodeop. The plugin will rebuild it as new `setabi` transactions are +applied or previously-unseen contracts are touched (lazy current-ABI fetch); +historical ABI lookup for events before the loss will fall back to raw hex. --- From d653e43de607f253bd22fbb1551ffb65d2fa5d20 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 14 Apr 2026 08:48:07 -0500 Subject: [PATCH 13/32] trace_api: skip lazy ABI fetch for setabi targets in same trx on_applied_transaction runs AFTER all actions in a transaction have been applied, so lazy-fetching the current chain-DB ABI on first encounter of an account produces the POST-setabi ABI when that account's ABI is being replaced in the same trx. Recording it as account@global_seq=0 would then be served by upper_bound step-back for any pre-setabi action on that account, decoding those actions with the wrong schema. Fix: scan action_traces once for setabi targets, then on the second pass skip the lazy fetch for any account whose ABI is being replaced in this trx. For the narrow edge case (never-before-observed contract with a setabi in its first-observed trx AND pre-setabi actions on it), those pre-setabi actions now return raw hex -- strictly safer than wrong data, and the correct behavior once any earlier trx has recorded an ABI. Also documents the caveat in trace_api_plugin.md and adds three extraction tests (skip-when-target, prior-ABI-survives, sibling-lazy- fetch-still-fires). --- .../sysio/trace_api/chain_extraction.hpp | 38 ++- .../trace_api_plugin/test/test_extraction.cpp | 217 +++++++++++++++++- plugins/trace_api_plugin/trace_api_plugin.md | 15 ++ 3 files changed, 264 insertions(+), 6 deletions(-) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp index 6fc38adce5..1e292d15d5 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp @@ -70,12 +70,42 @@ class chain_extraction_impl_type { } // ABI capture: scan all action traces (including inlines) in this transaction. + // + // First pass: collect setabi targets in this trx so the second pass can + // skip the lazy fetch for any account whose ABI is being replaced here. + // on_applied_transaction runs AFTER all actions in the trx have applied, + // so the chain DB already reflects the new ABI; doing a lazy fetch and + // recording it as target@0 would poison lookups for actions on target + // that executed earlier in this same trx (they need the OLD ABI, which + // is no longer reachable from post-apply state). + std::unordered_set setabi_targets_this_trx; + for (const auto& at : trace->action_traces) { + if (!at.receipt) continue; + if (at.act.account == chain::config::system_account_name && + at.act.name == chain::name("setabi")) { + try { + chain::name target; + chain::bytes abi_bytes; + auto ds = fc::datastream(at.act.data.data(), at.act.data.size()); + fc::raw::unpack(ds, target); + fc::raw::unpack(ds, abi_bytes); + setabi_targets_this_trx.insert(target.to_uint64_t()); + } catch (...) {} + } + } + + // Second pass: lazy fetch + setabi record, skipping lazy fetch for any + // account whose ABI is being replaced in this trx. for (const auto& at : trace->action_traces) { if (!at.receipt) continue; // skip context-free or failed actions - // Lazy ABI fetch: on first encounter of any account, record its current ABI at - // global_seq 0 ("captured before tracing began; exact version unknown"). - if (_abi_fetcher && _seen_accounts.insert(at.act.account.to_uint64_t()).second) { + const uint64_t account = at.act.account.to_uint64_t(); + const bool newly_seen = _seen_accounts.insert(account).second; + + // Lazy ABI fetch: on first encounter of an account (that isn't having + // its ABI replaced in this same trx), record its current ABI at + // global_seq 0 so pre-plugin-start actions still decode. + if (_abi_fetcher && newly_seen && setabi_targets_this_trx.count(account) == 0) { try { if (auto abi = _abi_fetcher(at.act.account)) store.append_abi(at.act.account, 0, std::move(*abi)); @@ -94,7 +124,7 @@ class chain_extraction_impl_type { store.append_abi(target_account, at.receipt->global_sequence, std::vector(abi_bytes.begin(), abi_bytes.end())); - // Mark target as seen so lazy fetch doesn't later overwrite with a stale entry. + // Mark target as seen so any later-trx lazy fetch doesn't overwrite. _seen_accounts.insert(target_account.to_uint64_t()); } catch (...) {} } diff --git a/plugins/trace_api_plugin/test/test_extraction.cpp b/plugins/trace_api_plugin/test/test_extraction.cpp index 9521cd8f84..afc5e6f08c 100644 --- a/plugins/trace_api_plugin/test/test_extraction.cpp +++ b/plugins/trace_api_plugin/test/test_extraction.cpp @@ -42,6 +42,28 @@ namespace { "sysio.token"_n, "transfer"_n, make_transfer_data( from, to, quantity, std::move(memo) ) ); } + // sysio::setabi action data: (name account, vector abi). The extraction path + // unpacks it via fc::raw in chain_extraction.hpp. + std::vector make_setabi_data( chain::name target, const std::vector& abi_bytes ) { + fc::datastream ps; + fc::raw::pack(ps, target, abi_bytes); + std::vector data( ps.tellp() ); + if (!data.empty()) { + fc::datastream ds(data.data(), data.size()); + fc::raw::pack(ds, target, abi_bytes); + } + return data; + } + + auto make_setabi_action( chain::name target, const std::vector& abi_bytes ) { + return chain::action( {}, chain::config::system_account_name, "setabi"_n, + make_setabi_data( target, abi_bytes ) ); + } + + auto make_simple_action( chain::name account, chain::name act_name ) { + return chain::action( {}, account, act_name, std::vector{} ); + } + auto make_packed_trx( std::vector actions ) { chain::signed_transaction trx; trx.actions = std::move( actions ); @@ -113,13 +135,19 @@ struct extraction_test_fixture { return std::nullopt; // no prior data in unit tests } - void append_abi(chain::name, uint64_t, std::vector) { - // not tested here; abi_log tests cover this + void append_abi(chain::name account, uint64_t global_seq, std::vector abi_bytes) { + fixture.abi_calls.push_back({account, global_seq, std::move(abi_bytes)}); } extraction_test_fixture& fixture; }; + struct abi_call { + chain::name account; + uint64_t global_seq = 0; + std::vector abi_bytes; + }; + extraction_test_fixture() : extraction_impl(mock_logfile_provider_type(*this), exception_handler{} ) { @@ -141,6 +169,7 @@ struct extraction_test_fixture { uint32_t max_lib = 0; std::vector data_log = {}; std::unordered_map> id_log; + std::vector abi_calls; chain_extraction_impl_type extraction_impl; }; @@ -349,4 +378,188 @@ BOOST_AUTO_TEST_SUITE(block_extraction) BOOST_REQUIRE_EQUAL(std::get(data_log.at(0)), expected_block_trace); } +BOOST_AUTO_TEST_SUITE_END() + + +// --------------------------------------------------------------------------- +// ABI capture: lazy fetch + setabi interaction +// --------------------------------------------------------------------------- + +struct abi_capture_fixture { + struct mock_store { + explicit mock_store(abi_capture_fixture& f) : fixture(f) {} + + template void append(const BT&) {} + void append_lib(uint32_t) {} + void append_trx_ids(const block_trxs_entry&) {} + std::optional first_recorded_block() const { return std::nullopt; } + std::optional last_recorded_block() const { return std::nullopt; } + + void append_abi(chain::name account, uint64_t global_seq, std::vector abi_bytes) { + fixture.abi_calls.push_back({account, global_seq, std::move(abi_bytes)}); + } + + abi_capture_fixture& fixture; + }; + + struct abi_call { + chain::name account; + uint64_t global_seq = 0; + std::vector abi_bytes; + }; + + using extraction_t = chain_extraction_impl_type; + + // Fetcher returns whatever is keyed in fetcher_state (mimics reading the + // chain DB's account_metadata_object post-apply). + std::map> fetcher_state; + + extraction_t::abi_fetcher_t make_fetcher() { + return [this](chain::name account) -> std::optional> { + auto it = fetcher_state.find(account); + if (it == fetcher_state.end()) return std::nullopt; + return it->second; + }; + } + + std::vector abi_calls; + + // Feed a pre-built transaction through the extraction impl. Returns the extraction + // so additional trxs can be fed through the same instance (mimicking multi-trx flows). + std::unique_ptr extraction = std::make_unique( + mock_store{*this}, exception_handler{}, make_fetcher()); + + void signal(const chain::transaction_trace_ptr& trace, const chain::packed_transaction_ptr& ptrx) { + extraction->signal_applied_transaction(trace, ptrx); + } +}; + + +BOOST_AUTO_TEST_SUITE(abi_capture_tests) + +// Trx = [X.foo, sysio.setabi(X, newAbi), X.bar]. X has NEVER been observed before. +// Without the fix: lazy fetch on iter 1 would read post-apply state (newAbi) and +// record X@0=newAbi, which would be served for X.foo's pre-setabi global_seq and +// decode with the wrong schema. Fix: scan for setabi targets first and skip +// lazy fetch for them. X.foo remains undecodable (there's no pre-setabi ABI +// anywhere reachable), but returning raw hex is strictly safer than wrong data. +BOOST_FIXTURE_TEST_CASE(lazy_fetch_skipped_for_same_trx_setabi_target, abi_capture_fixture) +{ + auto new_abi = std::vector{'n', 'e', 'w'}; + fetcher_state["x"_n] = new_abi; // what a post-apply lazy fetch would return + + auto x_foo_action = make_simple_action("x"_n, "foo"_n); + auto setabi_action = make_setabi_action("x"_n, new_abi); + auto x_bar_action = make_simple_action("x"_n, "bar"_n); + + auto x_foo = make_action_trace(100, x_foo_action, "x"_n); + auto setabi = make_action_trace(101, setabi_action, chain::config::system_account_name); + auto x_bar = make_action_trace(102, x_bar_action, "x"_n); + + auto ptrx = make_packed_trx({ x_foo_action, setabi_action, x_bar_action }); + auto trace = make_transaction_trace( + ptrx.id(), 1, 1, chain::transaction_receipt_header::executed, + { x_foo, setabi, x_bar }); + + signal(trace, std::make_shared(ptrx)); + + // Exactly one append: the setabi at its own global_sequence. + // No X@0 (the poisoning case), and NO X@100/102 (those are not setabis). + BOOST_REQUIRE_EQUAL(abi_calls.size(), 1u); + BOOST_TEST(abi_calls[0].account == "x"_n); + BOOST_TEST(abi_calls[0].global_seq == 101u); + BOOST_TEST(abi_calls[0].abi_bytes == new_abi); +} + +// Common case: X already has a prior setabi record (from an earlier trx), so +// _seen_accounts contains X. A later trx does [X.foo, setabi(X, newAbi), X.bar]. +// The lazy fetch is a no-op (X already seen); the setabi record is appended. +// After the trx, the in-memory log should contain BOTH records, so a lookup +// for X.foo's global_sequence (< setabi_seq) resolves to the old ABI via +// upper_bound step-back in abi_log. This is the fix's key property: it does +// not clobber the prior entry that lets pre-setabi actions decode correctly. +BOOST_FIXTURE_TEST_CASE(prior_setabi_survives_later_setabi_in_same_trx, abi_capture_fixture) +{ + auto old_abi = std::vector{'o', 'l', 'd'}; + auto new_abi = std::vector{'n', 'e', 'w'}; + + // Trx 1: the original setabi that registered X@50=old_abi. + { + auto setabi_old = make_setabi_action("x"_n, old_abi); + auto setabi_old_trace = make_action_trace(50, setabi_old, chain::config::system_account_name); + auto ptrx = make_packed_trx({ setabi_old }); + auto trace = make_transaction_trace( + ptrx.id(), 1, 1, chain::transaction_receipt_header::executed, + { setabi_old_trace }); + signal(trace, std::make_shared(ptrx)); + } + + BOOST_REQUIRE_EQUAL(abi_calls.size(), 1u); + BOOST_TEST(abi_calls[0].account == "x"_n); + BOOST_TEST(abi_calls[0].global_seq == 50u); + BOOST_TEST(abi_calls[0].abi_bytes == old_abi); + + // Fetcher now returns newAbi since in reality the chain DB has been updated. + fetcher_state["x"_n] = new_abi; + + // Trx 2: X.foo (ran under oldAbi), setabi(X, newAbi), X.bar (ran under newAbi). + { + auto x_foo_action = make_simple_action("x"_n, "foo"_n); + auto setabi_action = make_setabi_action("x"_n, new_abi); + auto x_bar_action = make_simple_action("x"_n, "bar"_n); + + auto x_foo = make_action_trace(200, x_foo_action, "x"_n); + auto setabi = make_action_trace(201, setabi_action, chain::config::system_account_name); + auto x_bar = make_action_trace(202, x_bar_action, "x"_n); + + auto ptrx = make_packed_trx({ x_foo_action, setabi_action, x_bar_action }); + auto trace = make_transaction_trace( + ptrx.id(), 1, 1, chain::transaction_receipt_header::executed, + { x_foo, setabi, x_bar }); + signal(trace, std::make_shared(ptrx)); + } + + // Expect exactly two appends total: the prior setabi plus the new one. + // No spurious X@0 lazy-fetch that would poison X.foo's lookup. + BOOST_REQUIRE_EQUAL(abi_calls.size(), 2u); + BOOST_TEST(abi_calls[1].account == "x"_n); + BOOST_TEST(abi_calls[1].global_seq == 201u); + BOOST_TEST(abi_calls[1].abi_bytes == new_abi); + + // Hand-check of the lookup contract (which abi_log tests cover end-to-end): + // lookup(X, 200) -> upper_bound finds X@201, step back to X@50 = OLD ABI. Correct. + // lookup(X, 201) -> upper_bound finds > X@201 (none), step back to X@201 = NEW. Correct. + // lookup(X, 202) -> same as above, NEW. Correct. +} + +// An unrelated account in the same trx as a setabi should still get a lazy +// fetch — the skip is narrowly scoped to the setabi target. +BOOST_FIXTURE_TEST_CASE(lazy_fetch_fires_for_non_setabi_target_in_same_trx, abi_capture_fixture) +{ + auto y_abi = std::vector{'y'}; + auto x_abi = std::vector{'x'}; + fetcher_state["y"_n] = y_abi; + + auto y_foo_action = make_simple_action("y"_n, "foo"_n); + auto setabi_action = make_setabi_action("x"_n, x_abi); + + auto y_foo = make_action_trace(300, y_foo_action, "y"_n); + auto setabi = make_action_trace(301, setabi_action, chain::config::system_account_name); + + auto ptrx = make_packed_trx({ y_foo_action, setabi_action }); + auto trace = make_transaction_trace( + ptrx.id(), 1, 1, chain::transaction_receipt_header::executed, + { y_foo, setabi }); + signal(trace, std::make_shared(ptrx)); + + // Two appends: Y@0 lazy fetch (Y isn't a setabi target) and X@301 setabi. + BOOST_REQUIRE_EQUAL(abi_calls.size(), 2u); + BOOST_TEST(abi_calls[0].account == "y"_n); + BOOST_TEST(abi_calls[0].global_seq == 0u); + BOOST_TEST(abi_calls[0].abi_bytes == y_abi); + BOOST_TEST(abi_calls[1].account == "x"_n); + BOOST_TEST(abi_calls[1].global_seq == 301u); + BOOST_TEST(abi_calls[1].abi_bytes == x_abi); +} + BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/plugins/trace_api_plugin/trace_api_plugin.md b/plugins/trace_api_plugin/trace_api_plugin.md index 542528da56..2503dca6a6 100644 --- a/plugins/trace_api_plugin/trace_api_plugin.md +++ b/plugins/trace_api_plugin/trace_api_plugin.md @@ -241,6 +241,21 @@ and `return_value` bytes of each action using the ABI captured in prevents a malformed ABI in one contract from breaking queries for unrelated actions in the same block. +### First-observation + same-trx setabi caveat + +If the plugin observes a contract for the *first* time via a transaction +that *also* contains a `setabi` for that same contract, actions on that +contract which executed *before* the setabi within that transaction cannot +be decoded and are returned as raw hex. The pre-setabi ABI is no longer +reachable from the post-apply chain state, so the plugin deliberately does +not record the post-apply (new) ABI as the contract's pre-observation +baseline — doing so would decode pre-setabi actions with the wrong schema. + +Once the contract has been observed at least once (via any earlier +transaction, or via a setabi-free transaction), later same-trx setabis do +not have this limitation: pre-setabi actions decode correctly with the +previously-recorded ABI. + When decoded `params` and `return_data` are present they appear alongside the raw `data` and `return_value` hex fields. From 4fb09b71018253d3498cf55ae87f0804309b67d1 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 14 Apr 2026 09:06:20 -0500 Subject: [PATCH 14/32] trace_api: drop unbounded _seen_accounts; use abi_log as truth for first-encounter The lazy-fetch path tracked an std::unordered_set of every account ever observed, growing without bound for the lifetime of the node. Naive LRU eviction is unsafe: re-fetching after eviction would overwrite an existing X@0 record with the post-any-interim-setabi ABI and poison pre-setabi lookups. Replace with abi_log::has_entry(account) -- if the log already records any (account, *) entry, lazy fetch is skipped. Memory is now bounded by the number of contracts that actually have an ABI captured (a few to a few thousand on realistic chains), not by everything-ever-seen. For the rare case of a contract account whose ABI is empty in chainbase, has_entry stays false and we re-fetch on every action -- one chainbase find_account_metadata call per action, microseconds. Same change converts uint64_t-typed name fields/keys throughout the plugin to chain::name (record_header.account, index_key, cache_key, setabi_targets_this_trx). chain::name is layout-compatible with uint64_t so the on-disk format is unchanged; the in-memory code is type-safe and drops .to_uint64_t() conversions. --- .../sysio/trace_api/abi_data_handler.hpp | 4 +-- .../include/sysio/trace_api/abi_log.hpp | 15 ++++++++--- .../sysio/trace_api/chain_extraction.hpp | 27 ++++++++++--------- .../sysio/trace_api/store_provider.hpp | 7 +++++ .../trace_api_plugin/src/abi_data_handler.cpp | 2 +- plugins/trace_api_plugin/src/abi_log.cpp | 14 +++++++--- .../trace_api_plugin/src/store_provider.cpp | 4 +++ .../trace_api_plugin/src/trace_api_plugin.cpp | 4 +++ .../trace_api_plugin/test/test_extraction.cpp | 12 +++++++++ 9 files changed, 66 insertions(+), 23 deletions(-) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp index 313ef6a85c..693a343c71 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp @@ -79,10 +79,10 @@ namespace sysio { // Returns nullptr if no ABI is available or construction failed. std::shared_ptr get_serializer(chain::name account, uint64_t global_seq); - using cache_key = std::pair; + using cache_key = std::pair; struct cache_key_hash { size_t operator()(const cache_key& k) const noexcept { - return std::hash{}(k.first) ^ (std::hash{}(k.second) << 1); + return std::hash{}(k.first) ^ (std::hash{}(k.second) << 1); } }; diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/abi_log.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/abi_log.hpp index 8d85aab217..1cf1115a18 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/abi_log.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/abi_log.hpp @@ -73,13 +73,20 @@ class abi_log { // Thread-safe; may run concurrently with append(). std::optional> lookup(chain::name account, uint64_t global_seq) const; + // Returns true if at least one record exists for the account at any + // global_sequence. Used by chain extraction to decide whether to lazy- + // fetch an ABI on first encounter. Thread-safe. + bool has_entry(chain::name account) const; + private: // Fixed-size record header on disk: (account, global_seq, blob_size). // Trailer is a u32 crc32 over (header + blob_bytes). + // chain::name is a uint64_t-wrapping struct, so this is 24 bytes with no + // padding and the on-disk wire format is identical to (u64, u64, u64). struct record_header { - uint64_t account = 0; - uint64_t global_seq = 0; - uint64_t blob_size = 0; + chain::name account; + uint64_t global_seq = 0; + uint64_t blob_size = 0; }; static_assert(sizeof(record_header) == 24); @@ -87,7 +94,7 @@ class abi_log { uint64_t blob_offset = 0; // file offset of blob_bytes (not the record_header) uint64_t blob_size = 0; }; - using index_key = std::pair; + using index_key = std::pair; // Walk the log file from the header onwards. Returns the offset of the // end of the last valid record. The file is truncated at that offset if diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp index 1e292d15d5..134b836c94 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp @@ -78,7 +78,7 @@ class chain_extraction_impl_type { // recording it as target@0 would poison lookups for actions on target // that executed earlier in this same trx (they need the OLD ABI, which // is no longer reachable from post-apply state). - std::unordered_set setabi_targets_this_trx; + std::unordered_set setabi_targets_this_trx; for (const auto& at : trace->action_traces) { if (!at.receipt) continue; if (at.act.account == chain::config::system_account_name && @@ -89,7 +89,7 @@ class chain_extraction_impl_type { auto ds = fc::datastream(at.act.data.data(), at.act.data.size()); fc::raw::unpack(ds, target); fc::raw::unpack(ds, abi_bytes); - setabi_targets_this_trx.insert(target.to_uint64_t()); + setabi_targets_this_trx.insert(target); } catch (...) {} } } @@ -99,16 +99,23 @@ class chain_extraction_impl_type { for (const auto& at : trace->action_traces) { if (!at.receipt) continue; // skip context-free or failed actions - const uint64_t account = at.act.account.to_uint64_t(); - const bool newly_seen = _seen_accounts.insert(account).second; + const chain::name account = at.act.account; // Lazy ABI fetch: on first encounter of an account (that isn't having // its ABI replaced in this same trx), record its current ABI at - // global_seq 0 so pre-plugin-start actions still decode. - if (_abi_fetcher && newly_seen && setabi_targets_this_trx.count(account) == 0) { + // global_seq 0 so pre-plugin-start actions still decode. "First + // encounter" is decided by the abi_log itself: if it has no record + // for this account, we trigger the fetch. Once any record exists + // (lazy or setabi), we never re-fetch. Using the log as + // source-of-truth avoids holding a per-node-lifetime set of all + // accounts ever observed. + if (_abi_fetcher + && setabi_targets_this_trx.count(account) == 0 + && !store.has_abi_entry(account)) + { try { - if (auto abi = _abi_fetcher(at.act.account)) - store.append_abi(at.act.account, 0, std::move(*abi)); + if (auto abi = _abi_fetcher(account)) + store.append_abi(account, 0, std::move(*abi)); } catch (...) {} } @@ -124,8 +131,6 @@ class chain_extraction_impl_type { store.append_abi(target_account, at.receipt->global_sequence, std::vector(abi_bytes.begin(), abi_bytes.end())); - // Mark target as seen so any later-trx lazy fetch doesn't overwrite. - _seen_accounts.insert(target_account.to_uint64_t()); } catch (...) {} } } @@ -230,8 +235,6 @@ class chain_extraction_impl_type { abi_fetcher_t _abi_fetcher; std::map cached_traces; std::optional onblock_trace; - // Track seen accounts (by raw uint64 name value) to avoid redundant lazy ABI fetches. - std::unordered_set _seen_accounts; bool _startup_checked{false}; }; diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp index daf5b20238..798247ad9c 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp @@ -372,6 +372,13 @@ namespace sysio::trace_api { */ std::optional> lookup_abi(chain::name account, uint64_t global_seq) const; + /** + * Return true if any ABI record exists for the account. Used by extraction + * to decide whether to trigger a lazy ABI fetch on first encounter. + * Thread-safe. + */ + bool has_abi_entry(chain::name account) const; + /** * Read the trace for a given block * @param block_height : the height of the data being read diff --git a/plugins/trace_api_plugin/src/abi_data_handler.cpp b/plugins/trace_api_plugin/src/abi_data_handler.cpp index 9760154d7d..c3c821c45d 100644 --- a/plugins/trace_api_plugin/src/abi_data_handler.cpp +++ b/plugins/trace_api_plugin/src/abi_data_handler.cpp @@ -5,7 +5,7 @@ namespace sysio::trace_api { std::shared_ptr abi_data_handler::get_serializer(chain::name account, uint64_t global_seq) { - const cache_key key{account.to_uint64_t(), global_seq}; + const cache_key key{account, global_seq}; { std::lock_guard lock(_cache_mtx); diff --git a/plugins/trace_api_plugin/src/abi_log.cpp b/plugins/trace_api_plugin/src/abi_log.cpp index 6fdca0594e..c7910d081a 100644 --- a/plugins/trace_api_plugin/src/abi_log.cpp +++ b/plugins/trace_api_plugin/src/abi_log.cpp @@ -178,7 +178,7 @@ uint64_t abi_log::recover_from_disk(const std::filesystem::path& path) { void abi_log::append(chain::name account, uint64_t global_seq, std::vector abi_bytes) { if (!_valid) return; - record_header rh{ account.to_uint64_t(), global_seq, abi_bytes.size() }; + record_header rh{ account, global_seq, abi_bytes.size() }; const uint32_t crc = compute_record_crc(rh, abi_bytes.data(), abi_bytes.size()); uint64_t blob_offset = 0; @@ -206,20 +206,26 @@ void abi_log::append(chain::name account, uint64_t global_seq, std::vector } } +bool abi_log::has_entry(chain::name account) const { + if (!_valid) return false; + std::lock_guard lock(_index_mtx); + auto it = _index.lower_bound({account, 0}); + return it != _index.end() && it->first.first == account; +} + std::optional> abi_log::lookup(chain::name account, uint64_t global_seq) const { if (!_valid) return std::nullopt; - const uint64_t acct = account.to_uint64_t(); uint64_t blob_offset = 0; uint64_t blob_size = 0; { std::lock_guard lock(_index_mtx); - auto it = _index.upper_bound({acct, global_seq}); + auto it = _index.upper_bound({account, global_seq}); if (it == _index.begin()) return std::nullopt; --it; - if (it->first.first != acct) + if (it->first.first != account) return std::nullopt; blob_offset = it->second.blob_offset; blob_size = it->second.blob_size; diff --git a/plugins/trace_api_plugin/src/store_provider.cpp b/plugins/trace_api_plugin/src/store_provider.cpp index 5e71dd00b4..bc96c07029 100644 --- a/plugins/trace_api_plugin/src/store_provider.cpp +++ b/plugins/trace_api_plugin/src/store_provider.cpp @@ -416,6 +416,10 @@ namespace sysio::trace_api { return _abi_log.lookup(account, global_seq); } + bool store_provider::has_abi_entry(chain::name account) const { + return _abi_log.has_entry(account); + } + uint32_t slice_directory::slice_number_from_path(const std::filesystem::path& trx_id_path) const { // Filename format: trace_trx_id_XXXXXXXXXX-YYYYYYYYYY.log // Parse the start block number (XXXXXXXXXX) and divide by _width. diff --git a/plugins/trace_api_plugin/src/trace_api_plugin.cpp b/plugins/trace_api_plugin/src/trace_api_plugin.cpp index d376c996b6..e63680d681 100644 --- a/plugins/trace_api_plugin/src/trace_api_plugin.cpp +++ b/plugins/trace_api_plugin/src/trace_api_plugin.cpp @@ -107,6 +107,10 @@ namespace { return store->lookup_abi(account, global_seq); } + bool has_abi_entry(chain::name account) const { + return store->has_abi_entry(account); + } + std::shared_ptr store; }; } diff --git a/plugins/trace_api_plugin/test/test_extraction.cpp b/plugins/trace_api_plugin/test/test_extraction.cpp index afc5e6f08c..a4ea309869 100644 --- a/plugins/trace_api_plugin/test/test_extraction.cpp +++ b/plugins/trace_api_plugin/test/test_extraction.cpp @@ -139,6 +139,12 @@ struct extraction_test_fixture { fixture.abi_calls.push_back({account, global_seq, std::move(abi_bytes)}); } + bool has_abi_entry(chain::name account) const { + for (const auto& c : fixture.abi_calls) + if (c.account == account) return true; + return false; + } + extraction_test_fixture& fixture; }; @@ -399,6 +405,12 @@ struct abi_capture_fixture { fixture.abi_calls.push_back({account, global_seq, std::move(abi_bytes)}); } + bool has_abi_entry(chain::name account) const { + for (const auto& c : fixture.abi_calls) + if (c.account == account) return true; + return false; + } + abi_capture_fixture& fixture; }; From 23dbdcbb0e5f87c62d1357d23f59f372e58d8264 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 14 Apr 2026 09:17:58 -0500 Subject: [PATCH 15/32] trace_api: validate trx_id index headers and bound the probe loop The trx_id index reader trusted bucket_count from the file header, so a corrupt or hostile index could: - allocate ~32 GB at startup (header.bucket_count = UINT32_MAX), - corrupt lookups (header.bucket_count not a power of two breaks the `& mask` math), - hang lookups forever (a fully-populated bucket array makes the probe-for-empty-slot loop never terminate). Validate at construction: - bucket_count must be a power of two (std::has_single_bit), - bucket_count must be <= 2^28 (~268M buckets, ~4 GB; covers any realistic slice configuration), - file size must equal sizeof(header) + bucket_count * sizeof(bucket). On any failure, mark the reader invalid; lookups return nullopt and get_trx_block_number falls back to the linear scan over the trx_id log. Bound the probe loop in lookup() at bucket_count iterations so even a hand-crafted file with no empty buckets terminates without hanging. While here, also bound --trace-slice-stride to [1, 1000000]. The default is 10000 and the practical realistic range tops out around 100K. The cap prevents an absurd configuration from making the per-slice block-offset sidecar pre-allocate gigabytes and from pushing trx_id bucket_count past the 2^28 cap added above. Adds 4 reader tests (non-power-of-two, above-cap, file-size mismatch, fully-populated table doesn't hang). --- .../trace_api_plugin/src/trace_api_plugin.cpp | 6 +- plugins/trace_api_plugin/src/trx_id_index.cpp | 44 +++++++- .../test/test_trx_id_index.cpp | 101 ++++++++++++++++++ plugins/trace_api_plugin/trace_api_plugin.md | 2 +- 4 files changed, 146 insertions(+), 7 deletions(-) diff --git a/plugins/trace_api_plugin/src/trace_api_plugin.cpp b/plugins/trace_api_plugin/src/trace_api_plugin.cpp index e63680d681..987b84b4b6 100644 --- a/plugins/trace_api_plugin/src/trace_api_plugin.cpp +++ b/plugins/trace_api_plugin/src/trace_api_plugin.cpp @@ -126,7 +126,9 @@ struct trace_api_common_impl { cfg_options("trace-dir", bpo::value()->default_value("traces"), "the location of the trace directory (absolute path or relative to application data dir)"); cfg_options("trace-slice-stride", bpo::value()->default_value(10'000), - "the number of blocks each \"slice\" of trace data will contain on the filesystem"); + "Number of blocks each \"slice\" of trace data will contain on the filesystem.\n" + "Must be in the range [1, 1000000]. Larger values reduce file count but bloat the\n" + "block-offset sidecar pre-allocation and stress the per-slice trx_id hash index."); cfg_options("trace-minimum-irreversible-history-blocks", boost::program_options::value()->default_value(-1), "Number of blocks to ensure are kept past LIB for retrieval before \"slice\" files can be automatically removed.\n" "A value of -1 indicates that automatic removal of \"slice\" files will be turned off."); @@ -150,6 +152,8 @@ struct trace_api_common_impl { resmon_plugin->monitor_directory(trace_dir); slice_stride = options.at("trace-slice-stride").as(); + SYS_ASSERT(slice_stride >= 1 && slice_stride <= 1'000'000, chain::plugin_config_exception, + "\"trace-slice-stride\" must be in [1, 1000000]; got {}", slice_stride); const int32_t blocks = options.at("trace-minimum-irreversible-history-blocks").as(); SYS_ASSERT(blocks >= -1, chain::plugin_config_exception, diff --git a/plugins/trace_api_plugin/src/trx_id_index.cpp b/plugins/trace_api_plugin/src/trx_id_index.cpp index 10f229d22b..0b20368204 100644 --- a/plugins/trace_api_plugin/src/trx_id_index.cpp +++ b/plugins/trace_api_plugin/src/trx_id_index.cpp @@ -63,6 +63,12 @@ uint64_t trx_id_index_reader::prefix_of(const chain::transaction_id_type& id) { return p; } +// Hard cap on bucket_count read from disk. At 16 bytes per bucket, 2^28 +// buckets = 4 GB. A realistic worst-case slice (default 10K blocks at +// ~1K trxs/block = 10M trxs at load factor 0.5 = 2^25 buckets) fits 8x +// inside this cap. Anything larger is treated as a corrupt/malicious file. +static constexpr uint32_t max_bucket_count = 1u << 28; + trx_id_index_reader::trx_id_index_reader(const std::filesystem::path& path) { try { fc::cfile f; @@ -85,6 +91,28 @@ trx_id_index_reader::trx_id_index_reader(const std::filesystem::path& path) { return; } + // Open-addressing math (mask = bucket_count - 1) requires a power of two. + if (!std::has_single_bit(header.bucket_count)) { + wlog("trace_api: trx_id index {} bucket_count {} is not a power of two, ignoring", + path.generic_string(), header.bucket_count); + return; + } + // Cap allocation against malicious / corrupt headers. + if (header.bucket_count > max_bucket_count) { + wlog("trace_api: trx_id index {} bucket_count {} exceeds cap {}, ignoring", + path.generic_string(), header.bucket_count, max_bucket_count); + return; + } + // File length must equal header + bucket_count * sizeof(bucket). + const uint64_t expected_size = sizeof(trx_id_index_header) + + uint64_t{header.bucket_count} * sizeof(trx_id_bucket); + const uint64_t actual_size = std::filesystem::file_size(path); + if (actual_size != expected_size) { + wlog("trace_api: trx_id index {} size {} != expected {}, ignoring", + path.generic_string(), actual_size, expected_size); + return; + } + _buckets.resize(header.bucket_count); for (auto& b : _buckets) { auto ds = f.create_datastream(); @@ -100,11 +128,17 @@ std::optional trx_id_index_reader::lookup(const chain::transaction_id_ if (!_valid || _buckets.empty()) return std::nullopt; - const uint64_t prefix = prefix_of(trx_id); - const uint32_t mask = static_cast(_buckets.size()) - 1; - uint32_t idx = static_cast(prefix) & mask; - - while (_buckets[idx].block_num != 0) { + const uint64_t prefix = prefix_of(trx_id); + const uint32_t bucket_count = static_cast(_buckets.size()); + const uint32_t mask = bucket_count - 1; + uint32_t idx = static_cast(prefix) & mask; + + // Bounded probe loop: a well-formed index has load factor <= 0.5 and an + // empty bucket terminates the chain. The bound guards against a corrupt + // file with no empty buckets at all (would otherwise loop forever). + for (uint32_t probes = 0; probes < bucket_count; ++probes) { + if (_buckets[idx].block_num == 0) + return std::nullopt; if (_buckets[idx].prefix64 == prefix) return _buckets[idx].block_num; idx = (idx + 1) & mask; diff --git a/plugins/trace_api_plugin/test/test_trx_id_index.cpp b/plugins/trace_api_plugin/test/test_trx_id_index.cpp index 0ecd7116bb..7c1431fedb 100644 --- a/plugins/trace_api_plugin/test/test_trx_id_index.cpp +++ b/plugins/trace_api_plugin/test/test_trx_id_index.cpp @@ -229,4 +229,105 @@ BOOST_FIXTURE_TEST_CASE(large_entry_set_all_found, trx_id_index_fixture) { } } +// --------------------------------------------------------------------------- +// Defensive validation of corrupt / hostile index files +// --------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_CASE(bucket_count_not_power_of_two_is_invalid, trx_id_index_fixture) { + fc::cfile f; + f.set_file_path(index_path()); + f.open(fc::cfile::create_or_update_rw_mode); + trx_id_index_header hdr; + hdr.bucket_count = 5; // not a power of two + auto data = fc::raw::pack(hdr); + f.write(data.data(), data.size()); + // Pad to expected size so the file-size check would otherwise pass. + trx_id_bucket empty{}; + for (int i = 0; i < 5; ++i) { + auto bdata = fc::raw::pack(empty); + f.write(bdata.data(), bdata.size()); + } + f.flush(); + f.close(); + + trx_id_index_reader r(index_path()); + BOOST_CHECK(!r.valid()); +} + +BOOST_FIXTURE_TEST_CASE(bucket_count_above_cap_is_invalid, trx_id_index_fixture) { + // Just write the header claiming an absurd bucket_count. We intentionally + // do NOT write the buckets payload (that would be ~4GB); the reader must + // reject the header before attempting to allocate. + fc::cfile f; + f.set_file_path(index_path()); + f.open(fc::cfile::create_or_update_rw_mode); + trx_id_index_header hdr; + hdr.bucket_count = (1u << 29); // beyond the cap of 1<<28 + auto data = fc::raw::pack(hdr); + f.write(data.data(), data.size()); + f.flush(); + f.close(); + + trx_id_index_reader r(index_path()); + BOOST_CHECK(!r.valid()); +} + +BOOST_FIXTURE_TEST_CASE(file_size_mismatch_is_invalid, trx_id_index_fixture) { + // Header claims 8 buckets but file only contains 2. + fc::cfile f; + f.set_file_path(index_path()); + f.open(fc::cfile::create_or_update_rw_mode); + trx_id_index_header hdr; + hdr.bucket_count = 8; + auto data = fc::raw::pack(hdr); + f.write(data.data(), data.size()); + trx_id_bucket empty{}; + for (int i = 0; i < 2; ++i) { + auto bdata = fc::raw::pack(empty); + f.write(bdata.data(), bdata.size()); + } + f.flush(); + f.close(); + + trx_id_index_reader r(index_path()); + BOOST_CHECK(!r.valid()); +} + +// Hand-craft a fully populated bucket array (load factor 1.0). A naive probe +// loop would spin forever looking for an empty slot. The bounded probe must +// terminate and return nullopt for a missing prefix. +BOOST_FIXTURE_TEST_CASE(lookup_terminates_on_full_table, trx_id_index_fixture) { + constexpr uint32_t bucket_count = 8; // power of two, small for the test + fc::cfile f; + f.set_file_path(index_path()); + f.open(fc::cfile::create_or_update_rw_mode); + trx_id_index_header hdr; + hdr.bucket_count = bucket_count; + auto hdr_data = fc::raw::pack(hdr); + f.write(hdr_data.data(), hdr_data.size()); + + // Fill EVERY bucket with a non-zero block_num and a unique non-matching + // prefix. Choose prefixes whose initial slot is bucket 0 so the probe + // walks the whole table looking for prefix 0xCAFEBABE... + for (uint32_t i = 0; i < bucket_count; ++i) { + trx_id_bucket b{}; + // (bucket_count is a power of two, so any prefix64 value has its slot + // determined by the low log2(bucket_count) bits — set those to 0.) + b.prefix64 = (uint64_t{i} << 8); // all start at slot 0, distinct values + b.block_num = 1000 + i; // non-zero + auto bdata = fc::raw::pack(b); + f.write(bdata.data(), bdata.size()); + } + f.flush(); + f.close(); + + trx_id_index_reader r(index_path()); + BOOST_REQUIRE(r.valid()); + + // Lookup a prefix that isn't stored. Must terminate (not hang) and return nullopt. + auto missing = make_trx_id(0xDEADBEEFCAFEBABEULL); + auto result = r.lookup(missing); + BOOST_CHECK(!result.has_value()); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/plugins/trace_api_plugin/trace_api_plugin.md b/plugins/trace_api_plugin/trace_api_plugin.md index 2503dca6a6..1baea538d6 100644 --- a/plugins/trace_api_plugin/trace_api_plugin.md +++ b/plugins/trace_api_plugin/trace_api_plugin.md @@ -79,7 +79,7 @@ default). | Option | Default | Description | |--------|---------|-------------| | `trace-dir` | `traces` | Directory for trace files. Relative paths are resolved from the node's data directory. | -| `trace-slice-stride` | `10000` | Number of blocks per slice file. Larger values reduce file count but increase the amount of data re-scanned when a single slice is accessed. | +| `trace-slice-stride` | `10000` | Number of blocks per slice file. Must be in `[1, 1000000]`. Larger values reduce file count but bloat the block-offset sidecar's per-slice pre-allocation (`stride * 8` bytes, sparse) and stress the per-slice trx_id hash index (rejected if it would need more than 2^28 buckets). | | `trace-minimum-irreversible-history-blocks` | `-1` | Blocks past LIB to retain before old slices can be auto-deleted. `-1` disables automatic deletion (keep forever). | | `trace-minimum-uncompressed-irreversible-history-blocks` | `-1` | Blocks past LIB to keep uncompressed. Slices older than this threshold are transparently compressed. `-1` disables automatic compression. | | `trace-max-block-range` | `100` | Maximum number of blocks scanned by a single `get_actions` or `get_token_transfers` request. `block_num_end` is silently clamped to `block_num_start + trace-max-block-range - 1`. Clients paginate by advancing `block_num_start` by this amount on each call. Set to `-1` to remove the cap entirely. | From 82527052b7c97e5a4512015a1e421600d4f0848e Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 14 Apr 2026 09:31:28 -0500 Subject: [PATCH 16/32] trace_api: dedup forked-out trxs in trx_id index build + writer overwrite The trx_id index path had two related correctness gaps that diverged from the linear-scan get_trx_block_number behavior: 1. trx_id_index_writer claimed "last write wins per prefix" but actually wrote the second entry to the next empty bucket, leaving both. The reader returned whichever the linear probe reached first -- usually the FIRST inserted entry, opposite of the documented intent. Fix the writer to also stop on prefix match and overwrite the existing slot. 2. build_trx_id_index iterated every block_trxs_entry in the trx_id log and added each (trx_id, block_num) pair, ignoring fork resolution. When a trx briefly appeared in a block that was later forked out (and possibly replaced by a different trx set at the same block_num, or the trx moved to a later block), the index would point at the forked-out block_num instead of the canonical one. Fix by first collapsing the log into a canonical map -- last block_trxs_entry per block_num wins, matching the linear scan's trx_block_nums.erase logic -- then feeding that to the writer. Updates the existing duplicate_prefix64_last_write_wins test to actually assert the lookup returns the latest value (was previously satisfied by "doesn't crash"), and adds an integration test that exercises three fork patterns: trx moved to a later block, trx removed entirely, and trx canonically at its original block. --- .../include/sysio/trace_api/trx_id_index.hpp | 4 +- .../trace_api_plugin/src/store_provider.cpp | 19 ++++++- plugins/trace_api_plugin/src/trx_id_index.cpp | 8 ++- .../trace_api_plugin/test/test_trace_file.cpp | 57 +++++++++++++++++++ .../test/test_trx_id_index.cpp | 24 +++++--- 5 files changed, 98 insertions(+), 14 deletions(-) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/trx_id_index.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/trx_id_index.hpp index 5850c941ad..f3d3227f74 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/trx_id_index.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/trx_id_index.hpp @@ -55,7 +55,9 @@ class trx_id_index_writer { private: static uint64_t prefix_of(const chain::transaction_id_type& id); - // last write wins per prefix (latest fork block overwrites earlier entries) + // (prefix64, block_num) pairs in insertion order. write() applies last- + // write-wins per prefix64 when populating the bucket array, so the latest + // add for a given prefix is what ends up in the on-disk hash table. std::vector> _entries; }; diff --git a/plugins/trace_api_plugin/src/store_provider.cpp b/plugins/trace_api_plugin/src/store_provider.cpp index bc96c07029..72b7caf0b1 100644 --- a/plugins/trace_api_plugin/src/store_provider.cpp +++ b/plugins/trace_api_plugin/src/store_provider.cpp @@ -452,7 +452,15 @@ namespace sysio::trace_api { log(std::string("Building trx_id index for slice: ") + std::to_string(slice_number)); - trx_id_index_writer writer; + // Dedup pass: the trx_id log can hold MULTIPLE block_trxs_entry records + // with the same block_num when the chain forks at that height (each + // accepted block, including forked-out ones, writes one entry). The + // last entry for each block_num reflects the canonical post-fork- + // resolution state, matching the semantics of the linear-scan path in + // get_trx_block_number which uses trx_block_nums.erase to drop forked- + // out entries. Build a canonical map first, then feed it to the + // writer (which itself does last-write-wins per prefix). + std::map> canonical; const uint64_t end = file_size(trx_id_file.get_file_path()); while (trx_id_file.tellp() < end) { metadata_log_entry entry; @@ -460,11 +468,16 @@ namespace sysio::trace_api { fc::raw::unpack(ds, entry); if (std::holds_alternative(entry)) { const auto& te = std::get(entry); - for (const auto& id : te.ids) - writer.add(id, te.block_num); + canonical[te.block_num] = te.ids; // last entry per block_num wins } } + trx_id_index_writer writer; + for (const auto& [block_num, ids] : canonical) { + for (const auto& id : ids) + writer.add(id, block_num); + } + // Write to a temp path and atomically rename so concurrent readers never // see a partially written index file. const auto tmp_path = idx_path.parent_path() / (idx_path.filename().string() + ".tmp"); diff --git a/plugins/trace_api_plugin/src/trx_id_index.cpp b/plugins/trace_api_plugin/src/trx_id_index.cpp index 0b20368204..f90000bfbc 100644 --- a/plugins/trace_api_plugin/src/trx_id_index.cpp +++ b/plugins/trace_api_plugin/src/trx_id_index.cpp @@ -30,9 +30,15 @@ void trx_id_index_writer::write(const std::filesystem::path& path) const { std::vector buckets(bucket_count); // zero-initialized = all empty + // Last-write-wins per prefix: probe forward until either an empty bucket + // (fresh insert) OR a bucket already holding this prefix (overwrite). + // Combined with the index-builder's per-block_num dedup pass, this means a + // trx that's been re-recorded under a different block_num after a fork + // resolves to the latest entry, matching the linear-scan get_trx_block_number + // path which returns *(--trx_block_nums.end()) (highest/most recent). for (const auto& [prefix, block_num] : _entries) { uint32_t idx = static_cast(prefix) & mask; - while (buckets[idx].block_num != 0) { + while (buckets[idx].block_num != 0 && buckets[idx].prefix64 != prefix) { idx = (idx + 1) & mask; } buckets[idx].prefix64 = prefix; diff --git a/plugins/trace_api_plugin/test/test_trace_file.cpp b/plugins/trace_api_plugin/test/test_trace_file.cpp index 77a6d126e0..c045d63931 100644 --- a/plugins/trace_api_plugin/test/test_trace_file.cpp +++ b/plugins/trace_api_plugin/test/test_trace_file.cpp @@ -153,6 +153,7 @@ namespace { } using store_provider::scan_metadata_log_from; using store_provider::read_data_log; + using store_provider::_slice_directory; }; class vslice_datastream; @@ -1183,4 +1184,60 @@ BOOST_AUTO_TEST_SUITE(slice_tests) BOOST_REQUIRE_EQUAL(*block_num, trx_block_num); // target trx is in final block } + // build_trx_id_index must apply the same fork-resolution logic as the linear + // scan in get_trx_block_number: when the trx_id log holds multiple + // block_trxs_entry records for the same block_num (one per accepted block at + // that height, including forked-out ones), only the LAST entry per block_num + // reflects the canonical post-fork state. Trxs that appeared in an earlier + // entry for that block_num but not the last one were forked out and must NOT + // appear in the index. Trxs that moved to a different block in the canonical + // fork must resolve to the new block_num. + BOOST_FIXTURE_TEST_CASE(test_build_trx_id_index_dedups_forked_trxs, test_fixture) + { + // Distinct first-8-byte prefixes so each trx maps to a unique bucket + // (the writer's prefix64 = first 8 bytes of the trx_id). + const chain::transaction_id_type trx_a = + "a1a2a3a4a5a6a7a8000000000000000000000000000000000000000000000001"_h; + const chain::transaction_id_type trx_b = + "b1b2b3b4b5b6b7b8000000000000000000000000000000000000000000000002"_h; + const chain::transaction_id_type trx_c = + "c1c2c3c4c5c6c7c8000000000000000000000000000000000000000000000003"_h; + + fc::temp_directory tempdir; + const uint32_t width = 100; + test_store_provider sp(tempdir.path(), width); + + // First accepted block at height 1: contains trx_a + trx_b. + sp.append_trx_ids(block_trxs_entry{ .ids = {trx_a, trx_b}, .block_num = 1 }); + // Block 1 forks out and is replaced by a different block at height 1: + // canonical block 1 contains only trx_c (trx_a moved, trx_b removed). + sp.append_trx_ids(block_trxs_entry{ .ids = {trx_c}, .block_num = 1 }); + // trx_a re-appears at block 2 in the canonical chain. + sp.append_trx_ids(block_trxs_entry{ .ids = {trx_a}, .block_num = 2 }); + + // Build the index for slice 0 directly (bypass the maintenance thread). + sp._slice_directory.build_trx_id_index(0, [](const std::string&){}); + + // Open the resulting on-disk index and verify lookups. + auto reader = sp._slice_directory.find_trx_id_index_slice(0); + BOOST_REQUIRE(reader.has_value()); + BOOST_REQUIRE(reader->valid()); + + // trx_a moved to block 2 in the canonical chain -> lookup returns 2, + // NOT 1 (the forked-out occurrence). + auto a = reader->lookup(trx_a); + BOOST_REQUIRE(a.has_value()); + BOOST_CHECK_EQUAL(*a, 2u); + + // trx_b was removed entirely (only present in the forked-out block 1). + // Its bucket must be empty. + auto b = reader->lookup(trx_b); + BOOST_CHECK(!b.has_value()); + + // trx_c is in canonical block 1. + auto c = reader->lookup(trx_c); + BOOST_REQUIRE(c.has_value()); + BOOST_CHECK_EQUAL(*c, 1u); + } + BOOST_AUTO_TEST_SUITE_END() diff --git a/plugins/trace_api_plugin/test/test_trx_id_index.cpp b/plugins/trace_api_plugin/test/test_trx_id_index.cpp index 7c1431fedb..ab742b45cd 100644 --- a/plugins/trace_api_plugin/test/test_trx_id_index.cpp +++ b/plugins/trace_api_plugin/test/test_trx_id_index.cpp @@ -134,25 +134,31 @@ BOOST_FIXTURE_TEST_CASE(linear_probing_on_prefix_collision, trx_id_index_fixture // --------------------------------------------------------------------------- BOOST_FIXTURE_TEST_CASE(duplicate_prefix64_last_write_wins, trx_id_index_fixture) { - // The writer can accumulate two entries with the same prefix64 (e.g., same - // first-8 bytes). The hash table stores both under different slots. The - // reader returns the *first* hit during linear probing — which is the - // first-inserted entry. Document this behavior so it doesn't surprise. + // Two adds with the same prefix64 should result in the second's block_num + // overwriting the first. Combined with build_trx_id_index's per-block_num + // dedup, this gives lookups the same "latest fork wins" semantic as the + // linear scan in get_trx_block_number. const uint64_t shared_prefix = 0xCAFEBABEDEADBEEFULL; auto id1 = make_trx_id(shared_prefix, 1); auto id2 = make_trx_id(shared_prefix, 2); // same prefix64, different tail trx_id_index_writer w; w.add(id1, 10); - w.add(id2, 20); // will probe to next empty slot + w.add(id2, 20); w.write(index_path()); trx_id_index_reader r(index_path()); BOOST_REQUIRE(r.valid()); - // Lookup finds first match on the shared prefix; result is either 10 or 20. - // The important thing is it doesn't crash or loop infinitely. - auto result = r.lookup(id1); - BOOST_CHECK(result.has_value()); + + // Both id1 and id2 share the prefix, so a lookup of either resolves the + // single bucket — and that bucket holds the LATEST value (20). + auto result1 = r.lookup(id1); + BOOST_REQUIRE(result1.has_value()); + BOOST_CHECK_EQUAL(*result1, 20u); + + auto result2 = r.lookup(id2); + BOOST_REQUIRE(result2.has_value()); + BOOST_CHECK_EQUAL(*result2, 20u); } // --------------------------------------------------------------------------- From 5bed6035c4f50081c742815499ecf2d4e042bc0e Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 14 Apr 2026 09:37:10 -0500 Subject: [PATCH 17/32] trace_api: doc says continuity gap shuts down the node (it does) The Startup continuity check section claimed gaps "do not prevent the node from running" -- but check_continuity in chain_extraction.hpp calls except_handler on a gap, which is wired to app().quit(), so the node actually shuts down. Update the doc to describe the real behavior: shut-down on gap, with operator-facing recovery steps (delete trace dir, load a snapshot covering the gap, or copy slices from another node). Also drops the stale "snapshot restore detected -> warning" row -- the code logs nothing on overlap, it just resumes silently. No code change. --- plugins/trace_api_plugin/trace_api_plugin.md | 40 ++++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/plugins/trace_api_plugin/trace_api_plugin.md b/plugins/trace_api_plugin/trace_api_plugin.md index 1baea538d6..a61884b98a 100644 --- a/plugins/trace_api_plugin/trace_api_plugin.md +++ b/plugins/trace_api_plugin/trace_api_plugin.md @@ -210,18 +210,34 @@ the lazy current-ABI fetch). ## Startup continuity check -On plugin startup the trace store's recorded block range is compared against -the chain's current head. Three outcomes are logged: - -| Situation | Log level | Description | -|-----------|-----------|-------------| -| No prior trace data | `info` | Fresh start; tracing begins at the current head. | -| Snapshot restore detected | `warning` | The snapshot skips blocks already in the trace store, creating a gap. Trace data for the skipped range is inaccessible. | -| Normal restart | `info` | Store is contiguous with the chain head; tracing resumes normally. | - -A continuity gap does not prevent the node from running, but `get_block` and -`get_transaction_trace` requests for block numbers inside the gap will return -404. +On the first `block_start` signal after plugin startup the trace store's +recorded block range is compared against the chain's current head, and the +plugin chooses one of four outcomes: + +| Situation | Behavior | +|-----------|----------| +| No prior trace data (empty slice dir) | `info` log, fresh start; tracing begins at the current head. | +| Chain head is within `[first_recorded, last_recorded + 1]` (exact continuation OR overlap from a snapshot replay) | Silent — re-applied blocks naturally overwrite existing slice entries. | +| Chain head is **before** the first recorded block | Plugin throws; `error` log, **node shuts down**. | +| Chain head is **after** `last_recorded + 1` (forward gap) | Plugin throws; `error` log, **node shuts down**. | + +The shutdown is intentional: a gap means the trace store is no longer a +faithful continuous record of chain history, and silently accepting it would +let `get_block` / `get_transaction_trace` return inconsistent data for blocks +on either side of the gap. + +To recover, pick one: + +- **Delete the trace directory and start fresh.** Tracing resumes from the + current chain head; old block traces are lost. +- **Load a snapshot whose chain head is within the existing recorded range + (or one block past it).** Replay covers the existing slice entries; tracing + continues with no gap. +- **Copy the missing slice files from another node** that has the missing + range, then restart. + +The check fires only on the first `block_start` after the plugin loads, so +recovery actions take effect on the next startup. --- From 07a24fa9e712b3460ab25d9aa85b197e540d016d Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 14 Apr 2026 10:16:18 -0500 Subject: [PATCH 18/32] trace_api: route all log output through shared "trace_api" logger; nitpicks Three small fixes bundled: 1. Promote the named "trace_api" logger to a shared header (include/sysio/trace_api/logging.hpp) so every translation unit in the plugin can route log output through it. Previously only trace_api_plugin.cpp used the named logger; abi_log, trx_id_index, store_provider, and chain_extraction logged via the default logger, so operator filtering on the "trace_api" entry in logging.json only affected a fraction of plugin output. All wlog/dlog/ilog/elog call sites in the plugin now go through fc_*log(_log, ...). Also adds diagnostic logs to the previously silent setabi catch blocks in chain_extraction so a malformed setabi action no longer disappears without trace. 2. slice_number_from_path returns std::optional on parse failure rather than letting std::stoul throw out of a public method. The caller in get_trx_block_number now falls back to its existing linear scan when the filename can't be parsed. Named-local return type chosen for NRVO. 3. etc/config/nodeop/aio/config.template.ini: replace the orphaned Trace API Plugin comment block with a short note that ABIs are now captured automatically (the prior trace-no-abis line was removed earlier in this PR but the surrounding context left it unclear what operator action was needed, if any -- answer: none). --- etc/config/nodeop/aio/config.template.ini | 3 ++ .../sysio/trace_api/chain_extraction.hpp | 18 +++++++--- .../include/sysio/trace_api/logging.hpp | 16 +++++++++ .../sysio/trace_api/store_provider.hpp | 6 ++-- plugins/trace_api_plugin/src/abi_log.cpp | 30 ++++++++-------- .../trace_api_plugin/src/store_provider.cpp | 34 +++++++++++++------ .../trace_api_plugin/src/trace_api_plugin.cpp | 10 +++--- plugins/trace_api_plugin/src/trx_id_index.cpp | 14 ++++---- 8 files changed, 86 insertions(+), 45 deletions(-) create mode 100644 plugins/trace_api_plugin/include/sysio/trace_api/logging.hpp diff --git a/etc/config/nodeop/aio/config.template.ini b/etc/config/nodeop/aio/config.template.ini index 33aeb5f645..a95df27215 100644 --- a/etc/config/nodeop/aio/config.template.ini +++ b/etc/config/nodeop/aio/config.template.ini @@ -46,4 +46,7 @@ p2p-server-address = 127.0.0.1:4444 max-clients = 150 # Trace API Plugin +# ABIs are captured automatically from observed setabi actions and lazy- +# fetched from chain state on first encounter -- no operator-supplied ABI +# files are required. trace-minimum-irreversible-history-blocks = -1 # Number of blocks to ensure are kept past LIB for retrieval before "slice" files can be automatically removed. A value of -1 indicates that automatic removal of "slice" files will be turned off. diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp index 134b836c94..259a95635b 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp @@ -2,10 +2,10 @@ #include #include +#include #include #include #include -#include #include #include #include @@ -90,7 +90,10 @@ class chain_extraction_impl_type { fc::raw::unpack(ds, target); fc::raw::unpack(ds, abi_bytes); setabi_targets_this_trx.insert(target); - } catch (...) {} + } catch (const std::exception& e) { + fc_wlog(_log, "trace_api: failed to unpack setabi data (collecting targets) at global_seq {}: {}", + at.receipt->global_sequence, e.what()); + } } } @@ -116,7 +119,9 @@ class chain_extraction_impl_type { try { if (auto abi = _abi_fetcher(account)) store.append_abi(account, 0, std::move(*abi)); - } catch (...) {} + } catch (const std::exception& e) { + fc_dlog(_log, "trace_api: lazy ABI fetch for {} failed: {}", account, e.what()); + } } // setabi: record the new ABI with its exact global_sequence. @@ -131,7 +136,10 @@ class chain_extraction_impl_type { store.append_abi(target_account, at.receipt->global_sequence, std::vector(abi_bytes.begin(), abi_bytes.end())); - } catch (...) {} + } catch (const std::exception& e) { + fc_wlog(_log, "trace_api: failed to record setabi at global_seq {}: {}", + at.receipt->global_sequence, e.what()); + } } } } @@ -157,7 +165,7 @@ class chain_extraction_impl_type { const auto first = store.first_recorded_block(); const auto last = store.last_recorded_block(); if (!first) { - ilog("trace_api: no prior trace data found, starting fresh at block {}", block_num); + fc_ilog(_log, "trace_api: no prior trace data found, starting fresh at block {}", block_num); return; } // Overlap or exact continuation: chain head is within or just past existing data. diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/logging.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/logging.hpp new file mode 100644 index 0000000000..4377ce00bf --- /dev/null +++ b/plugins/trace_api_plugin/include/sysio/trace_api/logging.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +namespace sysio::trace_api { + +// Shared "trace_api" logger. Configured by the plugin at startup via +// fc::logger::update(logger_name, _log) so operators control verbosity +// through the "trace_api" entry in logging.json. All trace_api_plugin +// log call sites should use this logger (fc_wlog(_log, ...), etc.) so +// that filtering applies uniformly across the plugin. +inline const std::string logger_name{"trace_api"}; +inline fc::logger _log; + +} // namespace sysio::trace_api diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp index 798247ad9c..12c24e96f5 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp @@ -237,9 +237,11 @@ namespace sysio::trace_api { /** * Derive the slice number from a trx_id slice file path. - * Parses the block-range start from the filename. + * Parses the block-range start from the filename. Returns nullopt if + * the filename does not parse (callers should fall back to a slower + * lookup path rather than skipping the file silently). */ - uint32_t slice_number_from_path(const std::filesystem::path& trx_id_path) const; + std::optional slice_number_from_path(const std::filesystem::path& trx_id_path) const; /** * Find the trx_id index file for a given slice number (or return nullopt if not present). diff --git a/plugins/trace_api_plugin/src/abi_log.cpp b/plugins/trace_api_plugin/src/abi_log.cpp index c7910d081a..ff721fae04 100644 --- a/plugins/trace_api_plugin/src/abi_log.cpp +++ b/plugins/trace_api_plugin/src/abi_log.cpp @@ -1,7 +1,7 @@ #include +#include #include -#include #include @@ -54,7 +54,7 @@ abi_log::abi_log(const std::filesystem::path& path) { try { _cfile.open(fc::cfile::create_or_update_rw_mode); } catch (...) { - wlog("trace_api: abi_log failed to open {}", path.generic_string()); + fc_wlog(_log, "trace_api: abi_log failed to open {}", path.generic_string()); return; } @@ -67,7 +67,7 @@ abi_log::abi_log(const std::filesystem::path& path) { _cfile.write(data.data(), data.size()); _cfile.flush(); } catch (...) { - wlog("trace_api: abi_log failed to write header to {}", path.generic_string()); + fc_wlog(_log, "trace_api: abi_log failed to write header to {}", path.generic_string()); return; } _end_offset = data.size(); @@ -82,17 +82,17 @@ abi_log::abi_log(const std::filesystem::path& path) { abi_log_header hdr; fc::raw::unpack(ds, hdr); if (hdr.magic != abi_log_header::magic_value) { - wlog("trace_api: abi_log {} has wrong magic {:#x}, ignoring", + fc_wlog(_log, "trace_api: abi_log {} has wrong magic {:#x}, ignoring", path.generic_string(), hdr.magic); return; } if (hdr.version != abi_log_header::current_version) { - wlog("trace_api: abi_log {} has unsupported version {}, ignoring", + fc_wlog(_log, "trace_api: abi_log {} has unsupported version {}, ignoring", path.generic_string(), hdr.version); return; } } catch (...) { - wlog("trace_api: abi_log {} header read failed", path.generic_string()); + fc_wlog(_log, "trace_api: abi_log {} header read failed", path.generic_string()); return; } @@ -110,14 +110,14 @@ uint64_t abi_log::recover_from_disk(const std::filesystem::path& path) { // Need at least record_header + crc trailer. if (file_size - record_start < sizeof(record_header) + record_trailer_size) { - wlog("trace_api: abi_log {} torn tail at offset {} (less than minimal record), truncating", + fc_wlog(_log, "trace_api: abi_log {} torn tail at offset {} (less than minimal record), truncating", path.generic_string(), record_start); break; } record_header rh{}; if (!pread_all(_cfile.fileno(), &rh, sizeof(rh), record_start)) { - wlog("trace_api: abi_log {} failed to read record header at offset {}, truncating", + fc_wlog(_log, "trace_api: abi_log {} failed to read record header at offset {}, truncating", path.generic_string(), record_start); break; } @@ -127,7 +127,7 @@ uint64_t abi_log::recover_from_disk(const std::filesystem::path& path) { const uint64_t record_end = crc_offset + record_trailer_size; if (record_end > file_size) { - wlog("trace_api: abi_log {} record at {} claims blob_size {} but file has only {} bytes remaining, truncating", + fc_wlog(_log, "trace_api: abi_log {} record at {} claims blob_size {} but file has only {} bytes remaining, truncating", path.generic_string(), record_start, rh.blob_size, file_size - blob_offset); break; } @@ -136,7 +136,7 @@ uint64_t abi_log::recover_from_disk(const std::filesystem::path& path) { if (rh.blob_size > 0) { blob.resize(rh.blob_size); if (!pread_all(_cfile.fileno(), blob.data(), rh.blob_size, blob_offset)) { - wlog("trace_api: abi_log {} failed to read blob at offset {}, truncating", + fc_wlog(_log, "trace_api: abi_log {} failed to read blob at offset {}, truncating", path.generic_string(), blob_offset); break; } @@ -144,14 +144,14 @@ uint64_t abi_log::recover_from_disk(const std::filesystem::path& path) { uint32_t stored_crc = 0; if (!pread_all(_cfile.fileno(), &stored_crc, sizeof(stored_crc), crc_offset)) { - wlog("trace_api: abi_log {} failed to read crc at offset {}, truncating", + fc_wlog(_log, "trace_api: abi_log {} failed to read crc at offset {}, truncating", path.generic_string(), crc_offset); break; } const uint32_t computed_crc = compute_record_crc(rh, blob.data(), rh.blob_size); if (computed_crc != stored_crc) { - wlog("trace_api: abi_log {} crc mismatch at record offset {} (stored {:#x} vs computed {:#x}), truncating", + fc_wlog(_log, "trace_api: abi_log {} crc mismatch at record offset {} (stored {:#x} vs computed {:#x}), truncating", path.generic_string(), record_start, stored_crc, computed_crc); break; } @@ -167,7 +167,7 @@ uint64_t abi_log::recover_from_disk(const std::filesystem::path& path) { std::error_code ec; std::filesystem::resize_file(path, offset, ec); if (ec) { - wlog("trace_api: abi_log {} failed to truncate to {}: {}", + fc_wlog(_log, "trace_api: abi_log {} failed to truncate to {}: {}", path.generic_string(), offset, ec.message()); } } @@ -192,7 +192,7 @@ void abi_log::append(chain::name account, uint64_t global_seq, std::vector _cfile.write(reinterpret_cast(&crc), sizeof(crc)); _cfile.flush(); } catch (...) { - wlog("trace_api: abi_log append failed at offset {}", _end_offset); + fc_wlog(_log, "trace_api: abi_log append failed at offset {}", _end_offset); return; } @@ -236,7 +236,7 @@ std::optional> abi_log::lookup(chain::name account, uint64_t g std::vector out(blob_size); if (!pread_all(_cfile.fileno(), out.data(), blob_size, blob_offset)) { - wlog("trace_api: abi_log pread of {} bytes at {} failed", blob_size, blob_offset); + fc_wlog(_log, "trace_api: abi_log pread of {} bytes at {} failed", blob_size, blob_offset); return std::nullopt; } return out; diff --git a/plugins/trace_api_plugin/src/store_provider.cpp b/plugins/trace_api_plugin/src/store_provider.cpp index 72b7caf0b1..2110824eda 100644 --- a/plugins/trace_api_plugin/src/store_provider.cpp +++ b/plugins/trace_api_plugin/src/store_provider.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -120,13 +121,18 @@ namespace sysio::trace_api { yield(); // Derive the slice number from the file path and try the index first. - const uint32_t slice_number = _slice_directory.slice_number_from_path(trx_id_file.get_file_path()); - if (auto reader = _slice_directory.find_trx_id_index_slice(slice_number)) { - if (auto block_num = reader->lookup(trx_id)) { - trx_block_nums.insert(*block_num); - return false; // found in an irreversible slice; stop + // If the filename can't be parsed (slice_number_from_path returns nullopt), + // fall through to the linear scan below. + const std::optional slice_number = + _slice_directory.slice_number_from_path(trx_id_file.get_file_path()); + if (slice_number) { + if (auto reader = _slice_directory.find_trx_id_index_slice(*slice_number)) { + if (auto block_num = reader->lookup(trx_id)) { + trx_block_nums.insert(*block_num); + return false; // found in an irreversible slice; stop + } + return true; // not in this indexed slice; continue to next } - return true; // not in this indexed slice; continue to next } // No index for this slice (reversible window or index not yet built): @@ -284,7 +290,7 @@ namespace sysio::trace_api { if (trace_found != index_found) { const std::string trace_status = trace_found ? "existing" : "new"; const std::string index_status = index_found ? "existing" : "new"; - elog("Trace file is {}, but it's metadata file is {}. This means the files are not consistent.", trace_status, index_status); + fc_elog(_log, "Trace file is {}, but it's metadata file is {}. This means the files are not consistent.", trace_status, index_status); } } @@ -420,13 +426,21 @@ namespace sysio::trace_api { return _abi_log.has_entry(account); } - uint32_t slice_directory::slice_number_from_path(const std::filesystem::path& trx_id_path) const { + std::optional slice_directory::slice_number_from_path(const std::filesystem::path& trx_id_path) const { // Filename format: trace_trx_id_XXXXXXXXXX-YYYYYYYYYY.log // Parse the start block number (XXXXXXXXXX) and divide by _width. + // Named local matching the return type so the compiler can NRVO the + // optional directly into the caller's slot. const auto name = trx_id_path.filename().string(); const auto prefix_len = std::char_traits::length(_trace_trx_id_prefix); - const uint32_t start_block = static_cast(std::stoul(name.substr(prefix_len, 10))); - return start_block / _width; + std::optional result; + try { + const uint32_t start_block = static_cast(std::stoul(name.substr(prefix_len, 10))); + result = start_block / _width; + } catch (...) { + fc_wlog(_log, "trace_api: cannot parse slice start-block from filename '{}'", name); + } + return result; } std::optional slice_directory::find_trx_id_index_slice(uint32_t slice_number) const { diff --git a/plugins/trace_api_plugin/src/trace_api_plugin.cpp b/plugins/trace_api_plugin/src/trace_api_plugin.cpp index 987b84b4b6..4ecdea157c 100644 --- a/plugins/trace_api_plugin/src/trace_api_plugin.cpp +++ b/plugins/trace_api_plugin/src/trace_api_plugin.cpp @@ -1,8 +1,9 @@ #include #include -#include #include +#include +#include #include #include @@ -17,9 +18,6 @@ using boost::signals2::scoped_connection; namespace { - const std::string logger_name("trace_api"); - fc::logger _log; - std::string to_detail_string(const std::exception_ptr& e) { try { std::rethrow_exception(e); @@ -220,7 +218,7 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_thismax_block_range; auto store = common->store; auto data_handler = std::make_shared( @@ -450,7 +448,7 @@ struct trace_api_plugin_impl { :common(common) {} void plugin_initialize(const appbase::variables_map& options) { - ilog("initializing trace api plugin"); + fc_ilog(_log, "initializing trace api plugin"); auto log_exceptions_and_shutdown = [](const exception_with_context& e) { log_exception(e, fc::log_level::error); app().quit(); diff --git a/plugins/trace_api_plugin/src/trx_id_index.cpp b/plugins/trace_api_plugin/src/trx_id_index.cpp index f90000bfbc..220f725439 100644 --- a/plugins/trace_api_plugin/src/trx_id_index.cpp +++ b/plugins/trace_api_plugin/src/trx_id_index.cpp @@ -1,8 +1,8 @@ #include +#include #include #include -#include #include #include @@ -84,11 +84,11 @@ trx_id_index_reader::trx_id_index_reader(const std::filesystem::path& path) { const auto header = extract_store(f); if (header.magic != trx_id_index_header::magic_value) { - wlog("trace_api: trx_id index {} has wrong magic, ignoring", path.generic_string()); + fc_wlog(_log,"trace_api: trx_id index {} has wrong magic, ignoring", path.generic_string()); return; } if (header.version != trx_id_index_header::current_version) { - wlog("trace_api: trx_id index {} has unsupported version {}, ignoring", + fc_wlog(_log,"trace_api: trx_id index {} has unsupported version {}, ignoring", path.generic_string(), header.version); return; } @@ -99,13 +99,13 @@ trx_id_index_reader::trx_id_index_reader(const std::filesystem::path& path) { // Open-addressing math (mask = bucket_count - 1) requires a power of two. if (!std::has_single_bit(header.bucket_count)) { - wlog("trace_api: trx_id index {} bucket_count {} is not a power of two, ignoring", + fc_wlog(_log,"trace_api: trx_id index {} bucket_count {} is not a power of two, ignoring", path.generic_string(), header.bucket_count); return; } // Cap allocation against malicious / corrupt headers. if (header.bucket_count > max_bucket_count) { - wlog("trace_api: trx_id index {} bucket_count {} exceeds cap {}, ignoring", + fc_wlog(_log,"trace_api: trx_id index {} bucket_count {} exceeds cap {}, ignoring", path.generic_string(), header.bucket_count, max_bucket_count); return; } @@ -114,7 +114,7 @@ trx_id_index_reader::trx_id_index_reader(const std::filesystem::path& path) { uint64_t{header.bucket_count} * sizeof(trx_id_bucket); const uint64_t actual_size = std::filesystem::file_size(path); if (actual_size != expected_size) { - wlog("trace_api: trx_id index {} size {} != expected {}, ignoring", + fc_wlog(_log,"trace_api: trx_id index {} size {} != expected {}, ignoring", path.generic_string(), actual_size, expected_size); return; } @@ -126,7 +126,7 @@ trx_id_index_reader::trx_id_index_reader(const std::filesystem::path& path) { } _valid = true; } catch (...) { - wlog("trace_api: failed to load trx_id index from {}", path.generic_string()); + fc_wlog(_log,"trace_api: failed to load trx_id index from {}", path.generic_string()); } } From c7849869be0ec60fe35c3a08aa149af6dbceb53d Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 14 Apr 2026 10:59:49 -0500 Subject: [PATCH 19/32] trace_api: nitpicks - bulk I/O, designated init, helper unification, naming Six small cleanups bundled: * trx_id_index reader/writer use bulk I/O (single read/write of the whole bucket array) instead of per-bucket fc::raw pack/unpack + datastream creation. Layout-equivalent on x86_64 LE; static_assert on bucket size guards the assumption. * test_trace_file fixture: replace IIFE-style imperative builders with C++20 designated-init aggregates (now that action_trace_v0 has 17 fields, positional aggregate init was unwieldy and the IIFE was worse). * request_handler: delete duplicated process_authorizations from .cpp; promote serialize_authorizations to an inline free function in the trace_api namespace; use it from both get_block (response_formatter) and the get_actions / get_token_transfers handlers. * request_handler: drop redundant `data.empty() ? "" : fc::to_hex(...)` conditionals (4 sites) -- fc::to_hex(ptr, 0) returns "" without dereferencing the pointer. * store_provider: compute _max_filename_size at compile time across every prefix and extension using std::max; adding a longer prefix later auto-grows the buffer instead of silently overflowing. * All headers converted from `namespace sysio { namespace trace_api {` to `namespace sysio::trace_api {` (C++17 nested form, matching the rest of the plugin). * Reserved padding fields renamed `_reserved` -> `reserved` (the leading-underscore convention is for private members; these are public on-disk struct fields). * Comment on the trx_id writer's bucket vector now explains that the zero-init is load-bearing for the empty-slot sentinel that terminates the probe loop. --- .../include/sysio/trace_api/abi_log.hpp | 4 +- .../sysio/trace_api/chain_extraction.hpp | 4 +- .../include/sysio/trace_api/data_log.hpp | 4 +- .../include/sysio/trace_api/extract_util.hpp | 4 +- .../include/sysio/trace_api/metadata_log.hpp | 4 +- .../sysio/trace_api/request_handler.hpp | 31 +++-- .../sysio/trace_api/store_provider.hpp | 4 +- .../include/sysio/trace_api/trace.hpp | 4 +- .../include/sysio/trace_api/trx_id_index.hpp | 8 +- .../trace_api_plugin/src/request_handler.cpp | 15 +- .../trace_api_plugin/src/store_provider.cpp | 18 ++- plugins/trace_api_plugin/src/trx_id_index.cpp | 24 ++-- .../trace_api_plugin/test/test_trace_file.cpp | 131 ++++++++++-------- 13 files changed, 140 insertions(+), 115 deletions(-) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/abi_log.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/abi_log.hpp index 1cf1115a18..902c25d79c 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/abi_log.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/abi_log.hpp @@ -51,7 +51,7 @@ struct abi_log_header { uint32_t magic = magic_value; uint32_t version = current_version; - uint64_t _reserved = 0; + uint64_t reserved = 0; }; static_assert(sizeof(abi_log_header) == 16); @@ -118,4 +118,4 @@ class abi_log { } // namespace sysio::trace_api -FC_REFLECT(sysio::trace_api::abi_log_header, (magic)(version)(_reserved)) +FC_REFLECT(sysio::trace_api::abi_log_header, (magic)(version)(reserved)) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp index 259a95635b..8a99dc9bbe 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp @@ -11,7 +11,7 @@ #include #include -namespace sysio { namespace trace_api { +namespace sysio::trace_api { using chain::transaction_id_type; using chain::packed_transaction; @@ -247,4 +247,4 @@ class chain_extraction_impl_type { }; -}} +} // namespace sysio::trace_api diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/data_log.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/data_log.hpp index e75daf50b0..0573a1214f 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/data_log.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/data_log.hpp @@ -4,10 +4,10 @@ #include #include -namespace sysio { namespace trace_api { +namespace sysio::trace_api { using data_log_entry = std::variant< block_trace_v0 >; -}} +} // namespace sysio::trace_api diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/extract_util.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/extract_util.hpp index 742fa502bf..d44596296d 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/extract_util.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/extract_util.hpp @@ -2,7 +2,7 @@ #include -namespace sysio { namespace trace_api { +namespace sysio::trace_api { inline action_trace_v0 to_action_trace( const chain::action_trace& at ) { action_trace_v0 r; @@ -68,4 +68,4 @@ inline block_trace_v0 create_block_trace( const chain::signed_block_ptr& block, return r; } -} } +} // namespace sysio::trace_api diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/metadata_log.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/metadata_log.hpp index cc7c60652a..6930d54bad 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/metadata_log.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/metadata_log.hpp @@ -4,7 +4,7 @@ #include #include -namespace sysio { namespace trace_api { +namespace sysio::trace_api { struct block_entry_v0 { chain::block_id_type id; uint32_t number = 0; @@ -21,7 +21,7 @@ namespace sysio { namespace trace_api { block_trxs_entry >; -}} +} // namespace sysio::trace_api FC_REFLECT(sysio::trace_api::block_entry_v0, (id)(number)(offset)); FC_REFLECT(sysio::trace_api::lib_entry_v0, (lib)); diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp index 37299e7aaa..2f941543a3 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp @@ -19,6 +19,19 @@ namespace sysio::trace_api { }; } + // Serialise an action's authorization vector to {actor, permission} JSON + // objects. Shared by get_block (response_formatter) and the get_actions + // / get_token_transfers handlers below. + inline fc::variants serialize_authorizations(const std::vector& auths) { + fc::variants result; + result.reserve(auths.size()); + for (const auto& a : auths) + result.emplace_back(fc::mutable_variant_object() + ("actor", a.actor.to_string()) + ("permission", a.permission.to_string())); + return result; + } + /** * Filter parameters for the get_actions endpoint. */ @@ -137,8 +150,8 @@ namespace sysio::trace_api { ("account", a.account.to_string()) ("name", a.action.to_string()) ("authorization", serialize_authorizations(a.authorization)) - ("data", a.data.empty() ? "" : fc::to_hex(a.data.data(), a.data.size())) - ("return_value", a.return_value.empty() ? "" : fc::to_hex(a.return_value.data(), a.return_value.size())) + ("data", fc::to_hex(a.data.data(), a.data.size())) + ("return_value", fc::to_hex(a.return_value.data(), a.return_value.size())) ("trx_id", trx.id.str()) ("block_num", trx.block_num) ("block_time", trx.block_time) @@ -181,8 +194,8 @@ namespace sysio::trace_api { ("account", a.account.to_string()) ("name", a.action.to_string()) ("authorization", serialize_authorizations(a.authorization)) - ("data", a.data.empty() ? "" : fc::to_hex(a.data.data(), a.data.size())) - ("return_value", a.return_value.empty() ? "" : fc::to_hex(a.return_value.data(), a.return_value.size())) + ("data", fc::to_hex(a.data.data(), a.data.size())) + ("return_value", fc::to_hex(a.return_value.data(), a.return_value.size())) ("trx_id", trx.id.str()) ("block_num", trx.block_num) ("block_time", trx.block_time) @@ -199,16 +212,6 @@ namespace sysio::trace_api { } private: - static fc::variants serialize_authorizations(const std::vector& auths) { - fc::variants result; - result.reserve(auths.size()); - for (const auto& a : auths) - result.emplace_back(fc::mutable_variant_object() - ("actor", a.actor.to_string()) - ("permission", a.permission.to_string())); - return result; - } - template actions_result get_actions_impl(const action_query& query, ActionVariantBuilder&& build_action_var) { actions_result result; diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp index 12c24e96f5..e83a5fec6d 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp @@ -108,7 +108,7 @@ namespace sysio::trace_api { uint32_t magic = magic_value; uint32_t version = current_version; uint32_t width = 0; // slice width (block count per slice) - uint32_t _reserved = 0; + uint32_t reserved = 0; }; static_assert(sizeof(blk_offset_index_header) == 16); @@ -505,4 +505,4 @@ namespace sysio::trace_api { } FC_REFLECT(sysio::trace_api::slice_directory::index_header, (version)) -FC_REFLECT(sysio::trace_api::blk_offset_index_header, (magic)(version)(width)(_reserved)) +FC_REFLECT(sysio::trace_api::blk_offset_index_header, (magic)(version)(width)(reserved)) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/trace.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/trace.hpp index edc2c92fb4..3c12836caa 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/trace.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/trace.hpp @@ -6,7 +6,7 @@ #include #include -namespace sysio { namespace trace_api { +namespace sysio::trace_api { struct authorization_trace_v0 { chain::name actor; @@ -71,7 +71,7 @@ namespace sysio { namespace trace_api { uint32_t block_num = 0; }; -} } +} // namespace sysio::trace_api FC_REFLECT(sysio::trace_api::authorization_trace_v0, (actor)(permission)) FC_REFLECT(sysio::trace_api::account_delta_v0, (account)(delta)) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/trx_id_index.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/trx_id_index.hpp index f3d3227f74..7c3ac18e7f 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/trx_id_index.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/trx_id_index.hpp @@ -31,14 +31,14 @@ struct trx_id_index_header { uint32_t magic = magic_value; uint32_t version = current_version; uint32_t bucket_count = 0; - uint32_t _reserved = 0; + uint32_t reserved = 0; }; static_assert(sizeof(trx_id_index_header) == 16); struct trx_id_bucket { uint64_t prefix64 = 0; // first 8 bytes of trx sha256 interpreted as uint64_t uint32_t block_num = 0; // 0 = empty; SYSIO block numbers start at 1 - uint32_t _reserved = 0; + uint32_t reserved = 0; }; static_assert(sizeof(trx_id_bucket) == 16); @@ -85,5 +85,5 @@ class trx_id_index_reader { } // namespace sysio::trace_api -FC_REFLECT(sysio::trace_api::trx_id_index_header, (magic)(version)(bucket_count)(_reserved)) -FC_REFLECT(sysio::trace_api::trx_id_bucket, (prefix64)(block_num)(_reserved)) +FC_REFLECT(sysio::trace_api::trx_id_index_header, (magic)(version)(bucket_count)(reserved)) +FC_REFLECT(sysio::trace_api::trx_id_bucket, (prefix64)(block_num)(reserved)) diff --git a/plugins/trace_api_plugin/src/request_handler.cpp b/plugins/trace_api_plugin/src/request_handler.cpp index 1514694c5d..3331392eea 100644 --- a/plugins/trace_api_plugin/src/request_handler.cpp +++ b/plugins/trace_api_plugin/src/request_handler.cpp @@ -11,19 +11,6 @@ namespace { return t.to_iso_string() + "Z"; } - fc::variants process_authorizations(const std::vector& authorizations) { - fc::variants result; - result.reserve(authorizations.size()); - for ( const auto& a: authorizations) { - result.emplace_back(fc::mutable_variant_object() - ("actor", a.actor.to_string()) - ("permission", a.permission.to_string()) - ); - } - - return result; - } - fc::variants process_actions(const std::vector& actions, const data_handler_function& data_handler) { fc::variants result; result.reserve(actions.size()); @@ -48,7 +35,7 @@ namespace { ("receiver", a.receiver.to_string()) ("account", a.account.to_string()) ("name", a.action.to_string()) - ("authorization", process_authorizations(a.authorization)) + ("authorization", serialize_authorizations(a.authorization)) ("data", fc::to_hex(a.data.data(), a.data.size())) ("return_value", fc::to_hex(a.return_value.data(), a.return_value.size())); diff --git a/plugins/trace_api_plugin/src/store_provider.cpp b/plugins/trace_api_plugin/src/store_provider.cpp index 2110824eda..0a391bf7f6 100644 --- a/plugins/trace_api_plugin/src/store_provider.cpp +++ b/plugins/trace_api_plugin/src/store_provider.cpp @@ -14,8 +14,22 @@ namespace { static constexpr const char* _trace_blk_idx_prefix = "trace_blk_idx_"; static constexpr const char* _trace_ext = ".log"; static constexpr const char* _compressed_trace_ext = ".clog"; - // longest prefix is "trace_trx_idx_" or "trace_blk_idx_" (14), then 10+1+10 digits, then ".clog" extension, then null - static constexpr int _max_filename_size = std::char_traits::length(_trace_trx_id_index_prefix) + 10 + 1 + 10 + std::char_traits::length(_compressed_trace_ext) + 1; + // Sized for the longest possible filename across every prefix and + // extension known to this file. Adding a longer prefix above + // automatically grows the buffer; no manual recount needed. + static constexpr size_t _max_prefix_length = std::max({ + std::char_traits::length(_trace_prefix), + std::char_traits::length(_trace_index_prefix), + std::char_traits::length(_trace_trx_id_prefix), + std::char_traits::length(_trace_trx_id_index_prefix), + std::char_traits::length(_trace_blk_idx_prefix), + }); + static constexpr size_t _max_ext_length = std::max( + std::char_traits::length(_trace_ext), + std::char_traits::length(_compressed_trace_ext) + ); + // prefix + 10-digit start + '-' + 10-digit end + ext + '\0' + static constexpr int _max_filename_size = _max_prefix_length + 10 + 1 + 10 + _max_ext_length + 1; std::string make_filename(const char* slice_prefix, const char* slice_ext, uint32_t slice_number, uint32_t slice_width) { char filename[_max_filename_size] = {}; diff --git a/plugins/trace_api_plugin/src/trx_id_index.cpp b/plugins/trace_api_plugin/src/trx_id_index.cpp index 220f725439..9c80e9cf85 100644 --- a/plugins/trace_api_plugin/src/trx_id_index.cpp +++ b/plugins/trace_api_plugin/src/trx_id_index.cpp @@ -28,7 +28,10 @@ void trx_id_index_writer::write(const std::filesystem::path& path) const { const uint32_t bucket_count = std::max(4u, std::bit_ceil(n * 2 + 1)); const uint32_t mask = bucket_count - 1; - std::vector buckets(bucket_count); // zero-initialized = all empty + // Value-initialize all buckets to zero. block_num == 0 is the empty-slot + // sentinel that terminates the probe loop below; do NOT replace this with + // reserve()+emplace_back() or any path that leaves block_num uninitialized. + std::vector buckets(bucket_count); // Last-write-wins per prefix: probe forward until either an empty bucket // (fresh insert) OR a bucket already holding this prefix (overwrite). @@ -54,10 +57,10 @@ void trx_id_index_writer::write(const std::filesystem::path& path) const { auto hdr_data = fc::raw::pack(header); f.write(hdr_data.data(), hdr_data.size()); - for (const auto& b : buckets) { - auto bkt_data = fc::raw::pack(b); - f.write(bkt_data.data(), bkt_data.size()); - } + // Bulk write: see the bulk-read note in trx_id_index_reader's constructor + // for the layout-equivalence rationale. + f.write(reinterpret_cast(buckets.data()), + buckets.size() * sizeof(trx_id_bucket)); f.flush(); } @@ -120,10 +123,13 @@ trx_id_index_reader::trx_id_index_reader(const std::filesystem::path& path) { } _buckets.resize(header.bucket_count); - for (auto& b : _buckets) { - auto ds = f.create_datastream(); - fc::raw::unpack(ds, b); - } + // Bulk read: trx_id_bucket is {u64, u32, u32} with no padding and + // static_assert(sizeof(trx_id_bucket) == 16) in the header. fc::raw::pack + // writes those fields little-endian, identical to the in-memory layout on + // x86_64 LE -- so a single read into the contiguous vector is equivalent + // to the field-by-field unpack and avoids the per-bucket call overhead. + f.read(reinterpret_cast(_buckets.data()), + static_cast(header.bucket_count) * sizeof(trx_id_bucket)); _valid = true; } catch (...) { fc_wlog(_log,"trace_api: failed to load trx_id index from {}", path.generic_string()); diff --git a/plugins/trace_api_plugin/test/test_trace_file.cpp b/plugins/trace_api_plugin/test/test_trace_file.cpp index c045d63931..075b788f69 100644 --- a/plugins/trace_api_plugin/test/test_trace_file.cpp +++ b/plugins/trace_api_plugin/test/test_trace_file.cpp @@ -12,28 +12,35 @@ using open_state = slice_directory::open_state; namespace { struct test_fixture { - std::vector actions = []{ - action_trace_v0 a0, a1, a2; - a0.global_sequence = 1; - a0.receiver = "receiver"_n; a0.account = "contract"_n; a0.action = "action"_n; - a0.authorization = {{ "alice"_n, "active"_n }}; - a0.data = { 0x01, 0x01, 0x01, 0x01 }; - a0.return_value = { 0x05, 0x05, 0x05, 0x05 }; - - a1.global_sequence = 0; - a1.receiver = "receiver"_n; a1.account = "contract"_n; a1.action = "action"_n; - a1.authorization = {{ "alice"_n, "active"_n }}; - a1.data = { 0x00, 0x00, 0x00, 0x00 }; - a1.return_value = { 0x04, 0x04, 0x04, 0x04 }; - - a2.global_sequence = 2; - a2.receiver = "receiver"_n; a2.account = "contract"_n; a2.action = "action"_n; - a2.authorization = {{ "alice"_n, "active"_n }}; - a2.data = { 0x02, 0x02, 0x02, 0x02 }; - a2.return_value = { 0x06, 0x06, 0x06, 0x06 }; - - return std::vector{a0, a1, a2}; - }(); + std::vector actions { + { + .global_sequence = 1, + .receiver = "receiver"_n, + .account = "contract"_n, + .action = "action"_n, + .authorization = {{ "alice"_n, "active"_n }}, + .data = { 0x01, 0x01, 0x01, 0x01 }, + .return_value = { 0x05, 0x05, 0x05, 0x05 } + }, + { + .global_sequence = 0, + .receiver = "receiver"_n, + .account = "contract"_n, + .action = "action"_n, + .authorization = {{ "alice"_n, "active"_n }}, + .data = { 0x00, 0x00, 0x00, 0x00 }, + .return_value = { 0x04, 0x04, 0x04, 0x04 } + }, + { + .global_sequence = 2, + .receiver = "receiver"_n, + .account = "contract"_n, + .action = "action"_n, + .authorization = {{ "alice"_n, "active"_n }}, + .data = { 0x02, 0x02, 0x02, 0x02 }, + .return_value = { 0x06, 0x06, 0x06, 0x06 } + } + }; transaction_trace_v0 transaction_trace { "0000000000000000000000000000000000000000000000000000000000000001"_h, @@ -70,42 +77,50 @@ namespace { } }; - const block_trace_v0 bt1 = []{ - action_trace_v0 at0, at1, at2; - at0.global_sequence = 0; - at0.receiver = "sysio.token"_n; at0.account = "sysio.token"_n; at0.action = "transfer"_n; - at0.authorization = {{ "alice"_n, "active"_n }}; - at0.data = make_transfer_data( "alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!" ); - - at1.global_sequence = 1; - at1.receiver = "alice"_n; at1.account = "sysio.token"_n; at1.action = "transfer"_n; - at1.authorization = {{ "alice"_n, "active"_n }}; - at1.data = make_transfer_data( "alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!" ); - - at2.global_sequence = 2; - at2.receiver = "bob"_n; at2.account = "sysio.token"_n; at2.action = "transfer"_n; - at2.authorization = {{ "alice"_n, "active"_n }}; - at2.data = make_transfer_data( "alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!" ); - - transaction_trace_v0 trx; - trx.id = "0000000000000000000000000000000000000000000000000000000000000001"_h; - trx.actions = {at0, at1, at2}; - trx.cpu_usage_us = 10; - trx.net_usage_words = 5; - trx.signatures = {chain::signature_type()}; - trx.trx_header = chain::transaction_header{chain::time_point_sec(), 1, 0, 100, 50, 0}; - - block_trace_v0 b; - b.id = "0000000000000000000000000000000000000000000000000000000000000001"_h; - b.number = 1; - b.previous_id = "0000000000000000000000000000000000000000000000000000000000000003"_h; - b.timestamp = chain::block_timestamp_type(1); - b.producer = "bp.one"_n; - b.transaction_mroot = "0000000000000000000000000000000000000000000000000000000000000000"_h; - b.finality_mroot = "0000000000000000000000000000000000000000000000000000000000000000"_h; - b.transactions = {trx}; - return b; - }(); + const block_trace_v0 bt1 { + .id = "0000000000000000000000000000000000000000000000000000000000000001"_h, + .number = 1, + .previous_id = "0000000000000000000000000000000000000000000000000000000000000003"_h, + .timestamp = chain::block_timestamp_type(1), + .producer = "bp.one"_n, + .transaction_mroot = "0000000000000000000000000000000000000000000000000000000000000000"_h, + .finality_mroot = "0000000000000000000000000000000000000000000000000000000000000000"_h, + .transactions = { + transaction_trace_v0 { + .id = "0000000000000000000000000000000000000000000000000000000000000001"_h, + .actions = { + { + .global_sequence = 0, + .receiver = "sysio.token"_n, + .account = "sysio.token"_n, + .action = "transfer"_n, + .authorization = {{ "alice"_n, "active"_n }}, + .data = make_transfer_data( "alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!" ) + }, + { + .global_sequence = 1, + .receiver = "alice"_n, + .account = "sysio.token"_n, + .action = "transfer"_n, + .authorization = {{ "alice"_n, "active"_n }}, + .data = make_transfer_data( "alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!" ) + }, + { + .global_sequence = 2, + .receiver = "bob"_n, + .account = "sysio.token"_n, + .action = "transfer"_n, + .authorization = {{ "alice"_n, "active"_n }}, + .data = make_transfer_data( "alice"_n, "bob"_n, "0.0001 SYS"_t, "Memo!" ) + } + }, + .cpu_usage_us = 10, + .net_usage_words = 5, + .signatures = {chain::signature_type()}, + .trx_header = chain::transaction_header{chain::time_point_sec(), 1, 0, 100, 50, 0} + } + } + }; const block_trace_v0 bt2 { "0000000000000000000000000000000000000000000000000000000000000002"_h, From 4c7c0a824f84f445984b570847126b275982c735 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 14 Apr 2026 14:17:34 -0500 Subject: [PATCH 20/32] trace_api: review fixes - bounded ranges, ABI cache correctness, notification opt-in Applies 30 fixes from the pre-PR review of feature/trace-api-history: Correctness - trace-max-block-range clamped to [1, 10000]; -1 rejected (was unbounded) - first_and_last_recorded_blocks() replaces two separate optionals so callers see a consistent view; NRVO in implementation - abi_data_handler cache keyed by effective ABI global_seq (via new abi_log::lookup_result) so bulk queries hit the cache - trx_id index hit confirmed against the block's block_trxs_entry before returning; collisions fall through to linear scan - get_transaction_trace scans raw transaction_trace_v0[] and builds the variant for the matching trx only; no more JSON-string round-trip API - get_actions: include_notifications flag (default false); when off and exactly one of account/receiver is set, the other is mirrored - Response envelope includes the actual block_num_start/end scanned - decode_error field surfaces ABI decode failures; params keep their decoded value when only return_value decode fails - On-disk magics reversed so a hex dump reads BLIX/TRIX/ABIL/WIRE Hygiene - File-scope constexpr _n literals for setabi / sysio.token / transfer - std::vector -> std::deque for trx_id_index_writer entries (growth cost) - abi_log: best-effort remove on open failure; blob_offset renamed to blob_file_offset; pread/stdio-flush note; append-side-is-single-threaded note; last-write-wins note at _index[...] sites - trace-max-block-range default raised from 100 to 1000 - Continuity-check error text names specific recovery remedies - Lazy ABI fetch exception logged at debug with account + message - Removed empty trace_api_rpc_plugin_impl::set_program_options and call sites - Log prefix "trace_api:" on plugin-init log lines - Miscellaneous struct alignment, comment clarifications, sort-stability note, yield_exception catch-order note Tests - test_continuity: new single-invocation-guarantee case with a non-throwing except_handler - Mocks updated for first_and_last_recorded_blocks / decode / lookup_result - test_trx_id_index uses id.data_size() - 1 instead of hardcoded 31 Docs - trace_api_plugin.md rewritten: user-facing sections first, on-disk layout moved to an "Implementation details" section at the end - etc/config/nodeop/aio/config.template.ini: commented trace-max-block-range line added Shared helper build_action_variant(action, decoded, variant_shape) moved from three near-duplicate inline builders to request_handler.cpp so get_actions, get_token_transfers, and process_block all agree. Verified: trace_api_plugin builds clean; test_trace_api_plugin passes all 97 test cases; nodeop links cleanly. --- etc/config/nodeop/aio/config.template.ini | 1 + .../chain/include/sysio/chain/snapshot.hpp | 5 +- .../sysio/trace_api/abi_data_handler.hpp | 75 +- .../include/sysio/trace_api/abi_log.hpp | 20 +- .../sysio/trace_api/chain_extraction.hpp | 42 +- .../sysio/trace_api/request_handler.hpp | 167 ++--- .../sysio/trace_api/store_provider.hpp | 42 +- .../include/sysio/trace_api/trx_id_index.hpp | 15 +- .../trace_api_plugin/src/abi_data_handler.cpp | 107 ++- plugins/trace_api_plugin/src/abi_log.cpp | 83 ++- .../trace_api_plugin/src/request_handler.cpp | 113 ++-- .../trace_api_plugin/src/store_provider.cpp | 84 ++- .../trace_api_plugin/src/trace_api_plugin.cpp | 103 +-- plugins/trace_api_plugin/src/trx_id_index.cpp | 8 + .../trace_api_plugin/test/test_abi_log.cpp | 32 +- .../trace_api_plugin/test/test_continuity.cpp | 35 +- .../test/test_data_handlers.cpp | 8 +- .../trace_api_plugin/test/test_extraction.cpp | 9 +- .../test/test_get_actions.cpp | 16 + .../test/test_trx_id_index.cpp | 2 +- plugins/trace_api_plugin/trace_api_plugin.md | 639 ++++++++++-------- 21 files changed, 962 insertions(+), 644 deletions(-) diff --git a/etc/config/nodeop/aio/config.template.ini b/etc/config/nodeop/aio/config.template.ini index a95df27215..85cf30b637 100644 --- a/etc/config/nodeop/aio/config.template.ini +++ b/etc/config/nodeop/aio/config.template.ini @@ -50,3 +50,4 @@ max-clients = 150 # fetched from chain state on first encounter -- no operator-supplied ABI # files are required. trace-minimum-irreversible-history-blocks = -1 # Number of blocks to ensure are kept past LIB for retrieval before "slice" files can be automatically removed. A value of -1 indicates that automatic removal of "slice" files will be turned off. +# trace-max-block-range = 1000 # Max blocks scanned per get_actions / get_token_transfers request. Must be in [1, 10000]. Clients paginate by advancing block_num_start on each call. diff --git a/libraries/chain/include/sysio/chain/snapshot.hpp b/libraries/chain/include/sysio/chain/snapshot.hpp index 65fc2f9239..ff0fce581a 100644 --- a/libraries/chain/include/sysio/chain/snapshot.hpp +++ b/libraries/chain/include/sysio/chain/snapshot.hpp @@ -30,7 +30,7 @@ namespace sysio { namespace chain { * * File format v1: * [Header] (8 bytes) - * magic: uint32_t (0x57495245 "WIRE") + * magic: uint32_t (0x45524957 "WIRE" — bytes 'W','I','R','E' on disk) * version: uint32_t (1) * * [Section Data] @@ -158,7 +158,8 @@ namespace sysio { namespace chain { class snapshot_writer { public: - static constexpr uint32_t magic_number = 0x57495245; // WIRE in ASCII + // Stored little-endian; bytes on disk are 'W','I','R','E' so a hex dump reads "WIRE". + static constexpr uint32_t magic_number = 0x45524957; static constexpr uint32_t max_threads = 4; class section_writer { diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp index 693a343c71..905c8e6d72 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp @@ -23,22 +23,48 @@ namespace sysio { * Data Handler that uses sysio::chain::abi_serializer to decode action data. * * ABIs are resolved dynamically via an abi_lookup_fn callback, typically backed - * by the abi_log on disk. Given (account, global_sequence) it returns the raw - * ABI bytes that were in effect at that point in chain history, or nullopt. + * by the abi_log on disk. Given (account, action_global_sequence) it returns + * {effective_abi_global_seq, abi_bytes} -- the setabi record that was in effect + * at that action, or nullopt. * - * A bounded LRU caches constructed abi_serializers by (account, global_seq) so - * bulk queries over the same contract do not rebuild the serializer per action. + * A bounded LRU caches constructed abi_serializers by (account, effective_abi_global_seq) + * so bulk queries that span many actions sharing the same ABI version hit the same + * cache entry instead of rebuilding the serializer per action. + * + * Decode results flag success/failure via abi_data_handler::decode_status so the + * caller can emit a decode_error field on failure while still returning the raw + * hex payload. * * Can be used directly as a Data_handler_provider OR shared between request_handlers * using the ::shared_provider abstraction. */ class abi_data_handler { public: - /// Callback: (account, global_sequence) -> raw ABI bytes in effect at that sequence, or nullopt. - /// Called on the HTTP thread; must be thread-safe. - using abi_lookup_fn = std::function>(chain::name, uint64_t)>; + struct lookup_entry { + uint64_t effective_global_seq = 0; + std::vector abi_bytes; + }; + + /// Callback: (account, action_global_sequence) -> {effective_abi_global_seq, abi_bytes} + /// for the ABI version in effect at that action. Called on the HTTP thread; must be thread-safe. + using abi_lookup_fn = std::function(chain::name, uint64_t)>; + + enum class decode_status { + not_attempted, // no ABI available for this action + ok, // decoded successfully + failed // ABI was available but decoding threw + }; + + struct decode_result { + decode_status status = decode_status::not_attempted; + fc::variant params; // decoded action data (empty on failure) + std::optional return_data; // decoded return_value (empty when no return type) + std::string error_message; // populated only when status == failed + }; - static constexpr size_t default_cache_capacity = 128; + // At ~500 KB per abi_serializer (for large contracts like sysio.system), + // 256 entries caps cache memory at roughly 130 MB in the worst case. + static constexpr size_t default_cache_capacity = 256; explicit abi_data_handler( exception_handler except_handler, abi_lookup_fn lookup_fn = {}, size_t cache_capacity = default_cache_capacity ) @@ -48,13 +74,16 @@ namespace sysio { {} /** - * Given an action trace, produce a tuple representing the `data` and `return_value` fields - * in the trace, decoded via the ABI in effect at that action's global_sequence. - * - * @param action - trace of the action including metadata necessary for finding the ABI - * @return tuple where the first element is a variant representing the decoded `data` field, - * and the second element represents the decoded `return_value` field. - * Both are empty variants when the ABI is unavailable or decoding fails. + * Decode the data + return_value of an action using the ABI that was in + * effect when it executed. Callers inspect decode_result::status to decide + * whether to emit params/return_data or a decode_error field. + */ + decode_result decode(const action_trace_v0& action); + + /** + * Legacy convenience wrapper: returns only {params, return_data} and drops + * the status/error fields. Retained for callers that still expect the + * tuple shape; prefer decode() for new code. */ std::tuple> serialize_to_variant(const std::variant& action); @@ -67,6 +96,10 @@ namespace sysio { :handler(handler) {} + decode_result decode(const action_trace_v0& action) { + return handler->decode(action); + } + std::tuple> serialize_to_variant( const std::variant& action ) { return handler->serialize_to_variant(action); } @@ -75,11 +108,13 @@ namespace sysio { }; private: - // Look up or construct the abi_serializer in effect for (account, global_seq). - // Returns nullptr if no ABI is available or construction failed. - std::shared_ptr get_serializer(chain::name account, uint64_t global_seq); + // Look up or construct the abi_serializer in effect for the action. Returns + // nullptr if no ABI is available or construction failed. The cache key is + // (account, effective_abi_global_seq) so multiple actions sharing an ABI + // version all hit the same entry. + std::shared_ptr get_serializer(chain::name account, uint64_t action_global_seq); - using cache_key = std::pair; + using cache_key = std::pair; struct cache_key_hash { size_t operator()(const cache_key& k) const noexcept { return std::hash{}(k.first) ^ (std::hash{}(k.second) << 1); @@ -89,7 +124,7 @@ namespace sysio { abi_lookup_fn _abi_lookup_fn; exception_handler except_handler; - // LRU cache of (account, global_seq) -> abi_serializer. + // LRU cache of (account, effective_abi_global_seq) -> abi_serializer. // _cache_list: MRU at front, LRU at back. _cache_map: key -> iterator into list. std::mutex _cache_mtx; std::list>> _cache_list; diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/abi_log.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/abi_log.hpp index 902c25d79c..b45cca9558 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/abi_log.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/abi_log.hpp @@ -46,11 +46,12 @@ namespace sysio::trace_api { // --------------------------------------------------------------------------- struct abi_log_header { - static constexpr uint32_t magic_value = 0x414C4942; + // Stored little-endian on disk so a hex dump of the first 4 bytes reads "ABIL". + static constexpr uint32_t magic_value = 0x4C494241; // bytes on disk: 'A','B','I','L' static constexpr uint32_t current_version = 1; - uint32_t magic = magic_value; - uint32_t version = current_version; + uint32_t magic = magic_value; + uint32_t version = current_version; uint64_t reserved = 0; }; static_assert(sizeof(abi_log_header) == 16); @@ -68,10 +69,17 @@ class abi_log { // (account, global_seq) keys. void append(chain::name account, uint64_t global_seq, std::vector abi_bytes); + struct lookup_result { + uint64_t effective_global_seq = 0; // global_seq of the ABI record that matched + std::vector abi_bytes; + }; + // Look up the ABI in effect for account at the largest recorded // global_seq <= the query. Returns nullopt if no record matches. + // The returned effective_global_seq is the global_seq the ABI was + // recorded at (used as a stable cache key by decoders). // Thread-safe; may run concurrently with append(). - std::optional> lookup(chain::name account, uint64_t global_seq) const; + std::optional lookup(chain::name account, uint64_t global_seq) const; // Returns true if at least one record exists for the account at any // global_sequence. Used by chain extraction to decide whether to lazy- @@ -91,8 +99,8 @@ class abi_log { static_assert(sizeof(record_header) == 24); struct index_entry { - uint64_t blob_offset = 0; // file offset of blob_bytes (not the record_header) - uint64_t blob_size = 0; + uint64_t blob_file_offset = 0; // file offset of blob_bytes (not the record_header) + uint64_t blob_size = 0; }; using index_key = std::pair; diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp index 8a99dc9bbe..3e8339bd72 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -15,6 +16,11 @@ namespace sysio::trace_api { using chain::transaction_id_type; using chain::packed_transaction; +using namespace sysio::chain::literals; + +// Compile-time constants for setabi detection: built from constexpr _n +// literals so we don't pay a chain::name construction cost on every action. +inline constexpr chain::name setabi_action_name = "setabi"_n; template class chain_extraction_impl_type { @@ -82,7 +88,7 @@ class chain_extraction_impl_type { for (const auto& at : trace->action_traces) { if (!at.receipt) continue; if (at.act.account == chain::config::system_account_name && - at.act.name == chain::name("setabi")) { + at.act.name == setabi_action_name) { try { chain::name target; chain::bytes abi_bytes; @@ -126,7 +132,7 @@ class chain_extraction_impl_type { // setabi: record the new ABI with its exact global_sequence. if (at.act.account == chain::config::system_account_name && - at.act.name == chain::name("setabi")) { + at.act.name == setabi_action_name) { try { chain::name target_account; chain::bytes abi_bytes; @@ -162,30 +168,42 @@ class chain_extraction_impl_type { void check_continuity(uint32_t block_num) { try { - const auto first = store.first_recorded_block(); - const auto last = store.last_recorded_block(); - if (!first) { + const auto recorded = store.first_and_last_recorded_blocks(); + if (!recorded) { fc_ilog(_log, "trace_api: no prior trace data found, starting fresh at block {}", block_num); return; } + const uint32_t first = recorded->first; + const uint32_t last = recorded->second; // Overlap or exact continuation: chain head is within or just past existing data. // Re-applied blocks will overwrite existing slice entries as they are re-recorded. - if (block_num >= *first && block_num <= *last + 1) + if (block_num >= first && block_num <= last + 1) return; - if (block_num < *first) { + if (block_num < first) { throw std::runtime_error( std::string("trace_api: chain head (") + std::to_string(block_num) + - ") is before the first recorded trace block (" + std::to_string(*first) + - "). Delete the trace directory and restart to record from the snapshot point."); + ") is before the first recorded trace block (" + std::to_string(first) + + "). To recover: load a snapshot whose chain head is within [" + + std::to_string(first) + ", " + std::to_string(last + 1) + + "], or copy the trace files covering blocks " + std::to_string(block_num) + + ".." + std::to_string(first - 1) + " from another node, or delete the " + "trace directory to start fresh (loses historical traces)."); } // block_num > last + 1: forward gap throw std::runtime_error( std::string("trace_api: gap detected in trace data. Last recorded block: ") + - std::to_string(*last) + ", current block: " + std::to_string(block_num) + - ". Delete the trace directory and restart to begin fresh, or load a snapshot " - "that continues from or before block " + std::to_string(*last + 1) + "."); + std::to_string(last) + ", current block: " + std::to_string(block_num) + + ". To recover: load a snapshot covering block " + std::to_string(last + 1) + + " (or earlier within the recorded range), or copy the trace files covering " + "blocks " + std::to_string(last + 1) + ".." + std::to_string(block_num - 1) + + " from another node, or delete the trace directory to start fresh " + "(loses historical traces)."); } catch (const yield_exception&) { + // Order matters: yield_exception propagates (it's the signal that the + // plugin's own except_handler uses to unwind the controller), while + // other exceptions from store.* calls or the throws above go through + // except_handler so the operator sees a properly formatted message. throw; } catch (...) { except_handler(MAKE_EXCEPTION_WITH_CONTEXT(std::current_exception())); diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp index 2f941543a3..c8ceaddbd4 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp @@ -32,6 +32,26 @@ namespace sysio::trace_api { return result; } + // ABI decode payload used by the shared variant builder below. Mirrors + // abi_data_handler::decode_result fields that end up in the HTTP response. + struct decoded_action { + fc::variant params; + std::optional return_data; + std::string error_message; + }; + + enum class variant_shape { + full, // get_actions / get_block: every field on action_trace_v0 + slim, // get_token_transfers: drops ordinals, receipt seqs, ram_deltas, resource usage + }; + + // Shared action->variant builder used by get_actions, get_token_transfers, + // and the legacy process_block path. Lives in request_handler.cpp so the + // formatting logic isn't duplicated in every template instantiation. + fc::mutable_variant_object build_action_variant(const action_trace_v0& a, + const decoded_action& decoded, + variant_shape shape); + /** * Filter parameters for the get_actions endpoint. */ @@ -98,29 +118,46 @@ namespace sysio::trace_api { */ fc::variant get_transaction_trace(chain::transaction_id_type trxid, uint32_t block_height){ _log("get_transaction_trace called" ); - fc::variant result = {}; - // extract the transaction trace from the block trace - auto resp = get_block_trace(block_height); - if (!resp.is_null()) { - auto& b_mvo = resp.get_object(); - if (b_mvo.contains("transactions")) { - auto& transactions = b_mvo["transactions"]; - std::string input_id = trxid.str(); - for (uint32_t i = 0; i < transactions.size(); ++i) { - if (transactions[i].is_null()) continue; - auto& t_mvo = transactions[i].get_object(); - if (t_mvo.contains("id")) { - const auto& t_id = t_mvo["id"].get_string(); - if (t_id == input_id) { - result = transactions[i]; - break; - } - } + // Named local with the exact return type so the compiler can NRVO it + // directly into the caller's slot. Scan the raw transaction_trace_v0[] + // for a matching id (cheap sha256 equality) and build the variant for + // ONLY the matching trx. The previous implementation materialised the + // full block variant (ABI-decoding every action) and then string-matched + // through the resulting JSON, which cost O(block) work per lookup. + fc::variant result; + + auto data = logfile_provider.get_block(block_height); + if (!data) { + _log("No block found at block height " + std::to_string(block_height)); + return result; + } + + auto data_handler = [this](const std::variant& action) -> std::tuple> { + return std::visit([&](const auto& a) { + return data_handler_provider.serialize_to_variant(a); + }, action); + }; + const bool irreversible = std::get<1>(*data); + + std::visit([&](const auto& block_trace) { + for (const auto& trx : block_trace.transactions) { + if (trx.id == trxid) { + // Build a single-transaction variant by calling the shared + // formatter on a synthesized block containing only this trx. + // Avoids decoding any other transactions in the block. + auto single = block_trace; // copy + single.transactions = {trx}; + auto block_var = detail::response_formatter::process_block( + data_log_entry{single}, irreversible, data_handler); + auto& txs = block_var.get_object()["transactions"]; + if (!txs.is_null() && txs.size() > 0) + result = txs[size_t{0}]; + return; } - if( result.is_null() ) - _log("Exhausted all " + std::to_string(transactions.size()) + " transactions in block " + b_mvo["number"].as_string() + " without finding trxid " + trxid.str()); } - } + _log("Transaction id " + trxid.str() + " not found in block " + std::to_string(block_height)); + }, std::get<0>(*data)); + return result; } @@ -135,85 +172,17 @@ namespace sysio::trace_api { * @return actions_result containing the matching actions */ actions_result get_actions(const action_query& query) { - return get_actions_impl(query, [this](const action_trace_v0& a, - const transaction_trace_v0& trx) { - auto action_var = fc::mutable_variant_object() - ("action_ordinal", a.action_ordinal) - ("creator_action_ordinal", a.creator_action_ordinal) - ("closest_unnotified_ancestor_action_ordinal", a.closest_unnotified_ancestor_action_ordinal) - ("global_sequence", a.global_sequence) - ("recv_sequence", a.recv_sequence) - ("auth_sequence", a.auth_sequence) - ("code_sequence", a.code_sequence) - ("abi_sequence", a.abi_sequence) - ("receiver", a.receiver.to_string()) - ("account", a.account.to_string()) - ("name", a.action.to_string()) - ("authorization", serialize_authorizations(a.authorization)) - ("data", fc::to_hex(a.data.data(), a.data.size())) - ("return_value", fc::to_hex(a.return_value.data(), a.return_value.size())) - ("trx_id", trx.id.str()) - ("block_num", trx.block_num) - ("block_time", trx.block_time) - ("producer_block_id", trx.producer_block_id); - - // account_ram_deltas - { - fc::variants deltas; - deltas.reserve(a.account_ram_deltas.size()); - for (const auto& d : a.account_ram_deltas) - deltas.emplace_back(fc::mutable_variant_object() - ("account", d.account.to_string()) - ("delta", d.delta)); - action_var("account_ram_deltas", std::move(deltas)); - } - - if (a.cpu_usage_us.has_value()) - action_var("cpu_usage_us", *a.cpu_usage_us); - if (a.net_usage.has_value()) - action_var("net_usage", *a.net_usage); - - auto [params, return_data] = data_handler_provider.serialize_to_variant(a); - if (!params.is_null()) - action_var("params", params); - if (return_data.has_value()) - action_var("return_data", *return_data); - - return action_var; - }); + return get_actions_impl(query, variant_shape::full); } /// Slim response for get_token_transfers: transfer-relevant fields only. /// Omits execution-tree ordinals, receipt sequences, ram_deltas, and resource usage. actions_result get_token_transfer_actions(const action_query& query) { - return get_actions_impl(query, [this](const action_trace_v0& a, - const transaction_trace_v0& trx) { - auto action_var = fc::mutable_variant_object() - ("global_sequence", a.global_sequence) - ("receiver", a.receiver.to_string()) - ("account", a.account.to_string()) - ("name", a.action.to_string()) - ("authorization", serialize_authorizations(a.authorization)) - ("data", fc::to_hex(a.data.data(), a.data.size())) - ("return_value", fc::to_hex(a.return_value.data(), a.return_value.size())) - ("trx_id", trx.id.str()) - ("block_num", trx.block_num) - ("block_time", trx.block_time) - ("producer_block_id", trx.producer_block_id); - - auto [params, return_data] = data_handler_provider.serialize_to_variant(a); - if (!params.is_null()) - action_var("params", params); - if (return_data.has_value()) - action_var("return_data", *return_data); - - return action_var; - }); + return get_actions_impl(query, variant_shape::slim); } private: - template - actions_result get_actions_impl(const action_query& query, ActionVariantBuilder&& build_action_var) { + actions_result get_actions_impl(const action_query& query, variant_shape shape) { actions_result result; const uint32_t end = query.block_num_end; @@ -230,6 +199,8 @@ namespace sysio::trace_api { // higher than later-scheduled notifications'. Sort pointers by // global_sequence so clients always see execution order (matches // chain_plugin's push_transaction and the legacy get_block response). + // global_sequence is unique per action (chain invariant), so sort + // stability is not required. std::vector sorted; sorted.reserve(trx.actions.size()); for (const auto& a : trx.actions) @@ -244,7 +215,17 @@ namespace sysio::trace_api { if (query.account && a.account != *query.account) continue; if (query.action && a.action != *query.action) continue; - result.actions.push_back(build_action_var(a, trx)); + // Decode via the provider; build the variant via the shared + // helper so get_actions / get_token_transfers / get_block all + // agree on field shapes. + auto dec = data_handler_provider.decode(a); + decoded_action da{std::move(dec.params), std::move(dec.return_data), std::move(dec.error_message)}; + fc::mutable_variant_object av = build_action_variant(a, da, shape); + av("trx_id", trx.id.str()) + ("block_num", trx.block_num) + ("block_time", trx.block_time) + ("producer_block_id", trx.producer_block_id); + result.actions.emplace_back(std::move(av)); } } }, std::get<0>(*data)); diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp index e83a5fec6d..5b3aebdb97 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp @@ -102,12 +102,13 @@ namespace sysio::trace_api { // // Native-endian, x86_64 Linux only (same convention as other slice files). struct blk_offset_index_header { - static constexpr uint32_t magic_value = 0x424C4958; // "BLIX" + // Stored little-endian on disk so a hex dump of the first 4 bytes reads "BLIX". + static constexpr uint32_t magic_value = 0x58494C42; // bytes on disk: 'B','L','I','X' static constexpr uint32_t current_version = 1; - uint32_t magic = magic_value; - uint32_t version = current_version; - uint32_t width = 0; // slice width (block count per slice) + uint32_t magic = magic_value; + uint32_t version = current_version; + uint32_t width = 0; // slice width (block count per slice) uint32_t reserved = 0; }; static_assert(sizeof(blk_offset_index_header) == 16); @@ -255,15 +256,12 @@ namespace sysio::trace_api { void build_trx_id_index(uint32_t slice_number, const log_handler& log); /** - * Return the lowest block number recorded in any index slice file, or nullopt if no data exists. + * Return {first, last} block numbers recorded across all index slice files, or nullopt + * if no data exists. Used at startup to detect gaps between existing trace data and the + * current chain head. Atomic in the sense that both values come from a single directory + * scan, so callers don't need to guard against seeing `first` but not `last`. */ - std::optional first_recorded_block() const; - - /** - * Return the highest block number recorded in any index slice file, or nullopt if no data exists. - * Used at startup to detect gaps between existing trace data and the current chain head. - */ - std::optional last_recorded_block() const; + std::optional> first_and_last_recorded_blocks() const; /** * Record the offset of a block's trace data in trace_.log, via the block-offset @@ -368,11 +366,15 @@ namespace sysio::trace_api { void append_abi(chain::name account, uint64_t global_seq, std::vector abi_bytes); /** - * Return the ABI bytes in effect for account at global_seq (the ABI with the + * Return the ABI in effect for account at global_seq (the ABI with the * largest recorded global_seq <= the query), or nullopt if none is found. + * The returned pair is {effective_global_seq, abi_bytes} where + * effective_global_seq is the recorded setabi's global_seq (0 for the + * lazy-capture sentinel). Decoders use effective_global_seq as a stable + * cache key so actions that share an ABI version all hit the same entry. * Thread-safe; may be called from the HTTP thread. */ - std::optional> lookup_abi(chain::name account, uint64_t global_seq) const; + std::optional lookup_abi(chain::name account, uint64_t global_seq) const; /** * Return true if any ABI record exists for the account. Used by extraction @@ -392,15 +394,11 @@ namespace sysio::trace_api { get_block_n get_trx_block_number(const chain::transaction_id_type& trx_id, const yield_function& yield= {}); /** - * Return the lowest block number recorded in any index slice file, or nullopt if no data exists. - */ - std::optional first_recorded_block() const; - - /** - * Return the highest block number recorded in any index slice file, or nullopt if the slice directory - * is empty. Used at startup to verify continuity between existing trace data and the current chain head. + * Return {first, last} block numbers recorded across all index slice files, or nullopt + * if the slice directory is empty. Used at startup to verify continuity between existing + * trace data and the current chain head. */ - std::optional last_recorded_block() const; + std::optional> first_and_last_recorded_blocks() const; void start_maintenance_thread( log_handler log ) { _slice_directory.start_maintenance_thread( std::move(log) ); diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/trx_id_index.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/trx_id_index.hpp index 7c3ac18e7f..1535d5edcd 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/trx_id_index.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/trx_id_index.hpp @@ -4,8 +4,10 @@ #include #include #include +#include #include #include +#include #include namespace sysio::trace_api { @@ -25,20 +27,21 @@ namespace sysio::trace_api { // --------------------------------------------------------------------------- struct trx_id_index_header { - static constexpr uint32_t magic_value = 0x54524958; // "TRIX" + // Stored little-endian on disk so a hex dump of the first 4 bytes reads "TRIX". + static constexpr uint32_t magic_value = 0x58495254; // bytes on disk: 'T','R','I','X' static constexpr uint32_t current_version = 1; uint32_t magic = magic_value; uint32_t version = current_version; uint32_t bucket_count = 0; - uint32_t reserved = 0; + uint32_t reserved = 0; }; static_assert(sizeof(trx_id_index_header) == 16); struct trx_id_bucket { - uint64_t prefix64 = 0; // first 8 bytes of trx sha256 interpreted as uint64_t + uint64_t prefix64 = 0; // first 8 bytes of trx sha256 interpreted as uint64_t uint32_t block_num = 0; // 0 = empty; SYSIO block numbers start at 1 - uint32_t reserved = 0; + uint32_t reserved = 0; }; static_assert(sizeof(trx_id_bucket) == 16); @@ -58,7 +61,9 @@ class trx_id_index_writer { // (prefix64, block_num) pairs in insertion order. write() applies last- // write-wins per prefix64 when populating the bucket array, so the latest // add for a given prefix is what ends up in the on-disk hash table. - std::vector> _entries; + // std::deque avoids the O(N) reallocation+copy of std::vector growth for + // multi-million-entry slices without needing an up-front reserve hint. + std::deque> _entries; }; // --------------------------------------------------------------------------- diff --git a/plugins/trace_api_plugin/src/abi_data_handler.cpp b/plugins/trace_api_plugin/src/abi_data_handler.cpp index c3c821c45d..1b03e01afe 100644 --- a/plugins/trace_api_plugin/src/abi_data_handler.cpp +++ b/plugins/trace_api_plugin/src/abi_data_handler.cpp @@ -4,8 +4,16 @@ namespace sysio::trace_api { - std::shared_ptr abi_data_handler::get_serializer(chain::name account, uint64_t global_seq) { - const cache_key key{account, global_seq}; + std::shared_ptr abi_data_handler::get_serializer(chain::name account, uint64_t action_global_seq) { + if (!_abi_lookup_fn) return nullptr; + + // Resolve the effective ABI version first. Using it as the cache key means + // N actions sharing the same setabi all hit the same entry; keying on the + // action's global_seq instead (as a prior impl did) defeats the cache. + auto lookup = _abi_lookup_fn(account, action_global_seq); + if (!lookup || lookup->abi_bytes.empty()) return nullptr; + + const cache_key key{account, lookup->effective_global_seq}; { std::lock_guard lock(_cache_mtx); @@ -17,17 +25,12 @@ namespace sysio::trace_api { } } - // Miss: look up ABI bytes and build the serializer outside the lock to avoid - // blocking other cache users during a potentially slow file read / unpack. - if (!_abi_lookup_fn) return nullptr; - - auto abi_bytes = _abi_lookup_fn(account, global_seq); - if (!abi_bytes || abi_bytes->empty()) return nullptr; - + // Miss: build the serializer outside the lock to avoid blocking other + // cache users during a potentially slow unpack. std::shared_ptr serializer; try { chain::abi_def abi; - auto ds = fc::datastream(abi_bytes->data(), abi_bytes->size()); + auto ds = fc::datastream(lookup->abi_bytes.data(), lookup->abi_bytes.size()); fc::raw::unpack(ds, abi); serializer = std::make_shared(std::move(abi), chain::abi_serializer::create_yield_function(fc::microseconds::maximum())); @@ -36,7 +39,7 @@ namespace sysio::trace_api { return nullptr; } - // Insert into cache. Another thread may have raced us — if so, return that entry. + // Insert into cache. Another thread may have raced us -- if so, return that entry. std::lock_guard lock(_cache_mtx); auto it = _cache_map.find(key); if (it != _cache_map.end()) { @@ -52,38 +55,68 @@ namespace sysio::trace_api { return serializer; } - std::tuple> abi_data_handler::serialize_to_variant(const std::variant& action) { - return std::visit([&](const auto& a) -> std::tuple> { - auto serializer = get_serializer(a.account, a.global_sequence); - if (!serializer) return {}; + abi_data_handler::decode_result abi_data_handler::decode(const action_trace_v0& a) { + // Named local return so NRVO constructs directly into the caller's slot. + decode_result result; - try { - auto type_name = serializer->get_action_type(a.action); - if (type_name.empty()) { - return {}; - } + auto serializer = get_serializer(a.account, a.global_sequence); + if (!serializer) return result; - // abi_serializer expects a yield function that takes a recursion depth - // abis are user provided, do not use a deadline - auto abi_yield = [](size_t recursion_depth) { - SYS_ASSERT( recursion_depth < chain::abi_serializer::max_recursion_depth, - chain::abi_recursion_depth_exception, - "exceeded max_recursion_depth {} ", chain::abi_serializer::max_recursion_depth ); - }; + auto type_name = serializer->get_action_type(a.action); + if (type_name.empty()) return result; - std::optional ret_data; - auto params = serializer->binary_to_variant(type_name, a.data, abi_yield); - if (a.return_value.size() > 0) { - auto return_type_name = serializer->get_action_result_type(a.action); - if (!return_type_name.empty()) { - ret_data = serializer->binary_to_variant(return_type_name, a.return_value, abi_yield); - } + // abis are user provided, do not use a deadline + auto abi_yield = [](size_t recursion_depth) { + SYS_ASSERT( recursion_depth < chain::abi_serializer::max_recursion_depth, + chain::abi_recursion_depth_exception, + "exceeded max_recursion_depth {} ", chain::abi_serializer::max_recursion_depth ); + }; + + // Separate try blocks so that a failure decoding return_value does not + // discard successfully-decoded params (and vice versa). + try { + result.params = serializer->binary_to_variant(type_name, a.data, abi_yield); + result.status = decode_status::ok; + } catch (const std::exception& e) { + result.status = decode_status::failed; + result.error_message = e.what(); + except_handler(MAKE_EXCEPTION_WITH_CONTEXT(std::current_exception())); + return result; + } catch (...) { + result.status = decode_status::failed; + result.error_message = "unknown exception decoding action data"; + except_handler(MAKE_EXCEPTION_WITH_CONTEXT(std::current_exception())); + return result; + } + + if (a.return_value.size() > 0) { + auto return_type_name = serializer->get_action_result_type(a.action); + if (!return_type_name.empty()) { + try { + result.return_data = serializer->binary_to_variant(return_type_name, a.return_value, abi_yield); + } catch (const std::exception& e) { + // Params decoded OK but return_value failed: keep params, flag failure + // and surface the error message. Callers can still emit params. + result.status = decode_status::failed; + result.error_message = std::string("return_value decode failed: ") + e.what(); + except_handler(MAKE_EXCEPTION_WITH_CONTEXT(std::current_exception())); + } catch (...) { + result.status = decode_status::failed; + result.error_message = "unknown exception decoding return_value"; + except_handler(MAKE_EXCEPTION_WITH_CONTEXT(std::current_exception())); } - return {std::move(params), std::move(ret_data)}; - } catch (...) { - except_handler(MAKE_EXCEPTION_WITH_CONTEXT(std::current_exception())); } + } + + return result; + } + std::tuple> abi_data_handler::serialize_to_variant(const std::variant& action) { + return std::visit([&](const auto& a) -> std::tuple> { + auto r = decode(a); + if (r.status == decode_status::ok) + return {std::move(r.params), std::move(r.return_data)}; + // failed or not_attempted -> legacy empty shape return {}; }, action); } diff --git a/plugins/trace_api_plugin/src/abi_log.cpp b/plugins/trace_api_plugin/src/abi_log.cpp index ff721fae04..40b870cb4c 100644 --- a/plugins/trace_api_plugin/src/abi_log.cpp +++ b/plugins/trace_api_plugin/src/abi_log.cpp @@ -55,15 +55,22 @@ abi_log::abi_log(const std::filesystem::path& path) { _cfile.open(fc::cfile::create_or_update_rw_mode); } catch (...) { fc_wlog(_log, "trace_api: abi_log failed to open {}", path.generic_string()); + // Best-effort clean-up: a failed open on some libc versions leaves a zero-byte + // file behind, which would look like a valid-but-header-less log on the next + // start and fail an unpack inside the existing-file branch. + std::error_code ec; + if (!existed) + std::filesystem::remove(path, ec); return; } if (!existed) { - // Fresh file: write header, no records yet. + // Fresh file: write header, no records yet. "ab+" mode always writes at + // EOF, so the seek below is advisory for the position indicator; the + // write lands at offset 0 by construction (file is freshly opened, EOF = 0). abi_log_header hdr; auto data = fc::raw::pack(hdr); try { - _cfile.seek(0); _cfile.write(data.data(), data.size()); _cfile.flush(); } catch (...) { @@ -104,6 +111,10 @@ uint64_t abi_log::recover_from_disk(const std::filesystem::path& path) { const uint64_t header_size = sizeof(abi_log_header); const uint64_t file_size = std::filesystem::file_size(path); + // pread correctness: fc::cfile is backed by buffered stdio (FILE*), but + // every append() flushes before returning, and the recover scan only runs + // during the constructor (before any concurrent append). Never introduce + // unflushed buffered writes on the same cfile or pread may see stale data. uint64_t offset = header_size; while (offset < file_size) { const uint64_t record_start = offset; @@ -122,22 +133,22 @@ uint64_t abi_log::recover_from_disk(const std::filesystem::path& path) { break; } - const uint64_t blob_offset = record_start + sizeof(record_header); - const uint64_t crc_offset = blob_offset + rh.blob_size; - const uint64_t record_end = crc_offset + record_trailer_size; + const uint64_t blob_file_offset = record_start + sizeof(record_header); + const uint64_t crc_offset = blob_file_offset + rh.blob_size; + const uint64_t record_end = crc_offset + record_trailer_size; if (record_end > file_size) { fc_wlog(_log, "trace_api: abi_log {} record at {} claims blob_size {} but file has only {} bytes remaining, truncating", - path.generic_string(), record_start, rh.blob_size, file_size - blob_offset); + path.generic_string(), record_start, rh.blob_size, file_size - blob_file_offset); break; } std::vector blob; if (rh.blob_size > 0) { blob.resize(rh.blob_size); - if (!pread_all(_cfile.fileno(), blob.data(), rh.blob_size, blob_offset)) { + if (!pread_all(_cfile.fileno(), blob.data(), rh.blob_size, blob_file_offset)) { fc_wlog(_log, "trace_api: abi_log {} failed to read blob at offset {}, truncating", - path.generic_string(), blob_offset); + path.generic_string(), blob_file_offset); break; } } @@ -156,7 +167,8 @@ uint64_t abi_log::recover_from_disk(const std::filesystem::path& path) { break; } - _index[{rh.account, rh.global_seq}] = index_entry{blob_offset, rh.blob_size}; + // std::map::operator[] — duplicate (account, global_seq) silently overwrites (last-write-wins). + _index[{rh.account, rh.global_seq}] = index_entry{blob_file_offset, rh.blob_size}; offset = record_end; } @@ -175,17 +187,22 @@ uint64_t abi_log::recover_from_disk(const std::filesystem::path& path) { return offset; } +// NOTE: callers of append()/has_entry() are expected to be single-threaded +// (the chain extraction thread). The append+index-insert sequence is not +// strictly atomic across the two mutexes; a concurrent caller could slip an +// insert for the same key between our write and our index update, producing +// a duplicate record. This is harmless given last-write-wins but not obvious. void abi_log::append(chain::name account, uint64_t global_seq, std::vector abi_bytes) { if (!_valid) return; record_header rh{ account, global_seq, abi_bytes.size() }; const uint32_t crc = compute_record_crc(rh, abi_bytes.data(), abi_bytes.size()); - uint64_t blob_offset = 0; + uint64_t blob_file_offset = 0; { std::lock_guard lock(_append_mtx); try { - _cfile.seek(_end_offset); + // "ab+" mode always writes at EOF; explicit seek would be a no-op here. _cfile.write(reinterpret_cast(&rh), sizeof(rh)); if (rh.blob_size > 0) _cfile.write(abi_bytes.data(), abi_bytes.size()); @@ -196,13 +213,14 @@ void abi_log::append(chain::name account, uint64_t global_seq, std::vector return; } - blob_offset = _end_offset + sizeof(rh); + blob_file_offset = _end_offset + sizeof(rh); _end_offset += sizeof(rh) + rh.blob_size + record_trailer_size; } { std::lock_guard lock(_index_mtx); - _index[{rh.account, rh.global_seq}] = index_entry{blob_offset, rh.blob_size}; + // std::map::operator[] — duplicate (account, global_seq) silently overwrites (last-write-wins). + _index[{rh.account, rh.global_seq}] = index_entry{blob_file_offset, rh.blob_size}; } } @@ -213,33 +231,44 @@ bool abi_log::has_entry(chain::name account) const { return it != _index.end() && it->first.first == account; } -std::optional> abi_log::lookup(chain::name account, uint64_t global_seq) const { - if (!_valid) return std::nullopt; +std::optional abi_log::lookup(chain::name account, uint64_t global_seq) const { + // Named local return type so the compiler can NRVO it straight into the + // caller's slot. + std::optional result; + if (!_valid) return result; - uint64_t blob_offset = 0; - uint64_t blob_size = 0; + uint64_t blob_file_offset = 0; + uint64_t blob_size = 0; + uint64_t effective_seq = 0; { std::lock_guard lock(_index_mtx); auto it = _index.upper_bound({account, global_seq}); if (it == _index.begin()) - return std::nullopt; + return result; --it; if (it->first.first != account) - return std::nullopt; - blob_offset = it->second.blob_offset; - blob_size = it->second.blob_size; + return result; + blob_file_offset = it->second.blob_file_offset; + blob_size = it->second.blob_size; + effective_seq = it->first.second; } - if (blob_size == 0) - return std::vector{}; + if (blob_size == 0) { + result.emplace(lookup_result{effective_seq, {}}); + return result; + } + // pread correctness: every append() flushes before returning, so the bytes + // we're about to read are visible to the underlying fd. Don't introduce + // unflushed buffered writes on the same cfile. std::vector out(blob_size); - if (!pread_all(_cfile.fileno(), out.data(), blob_size, blob_offset)) { - fc_wlog(_log, "trace_api: abi_log pread of {} bytes at {} failed", blob_size, blob_offset); - return std::nullopt; + if (!pread_all(_cfile.fileno(), out.data(), blob_size, blob_file_offset)) { + fc_wlog(_log, "trace_api: abi_log pread of {} bytes at {} failed", blob_size, blob_file_offset); + return result; } - return out; + result.emplace(lookup_result{effective_seq, std::move(out)}); + return result; } } // namespace sysio::trace_api diff --git a/plugins/trace_api_plugin/src/request_handler.cpp b/plugins/trace_api_plugin/src/request_handler.cpp index 3331392eea..d8c2b3a38d 100644 --- a/plugins/trace_api_plugin/src/request_handler.cpp +++ b/plugins/trace_api_plugin/src/request_handler.cpp @@ -4,6 +4,57 @@ #include +namespace sysio::trace_api { + +fc::mutable_variant_object build_action_variant(const action_trace_v0& a, + const decoded_action& decoded, + variant_shape shape) { + fc::mutable_variant_object v; + // Fields common to all shapes. + v("global_sequence", a.global_sequence) + ("receiver", a.receiver.to_string()) + ("account", a.account.to_string()) + ("name", a.action.to_string()) + ("authorization", serialize_authorizations(a.authorization)) + ("data", fc::to_hex(a.data.data(), a.data.size())) + ("return_value", fc::to_hex(a.return_value.data(), a.return_value.size())); + + if (shape == variant_shape::full) { + v("action_ordinal", a.action_ordinal) + ("creator_action_ordinal", a.creator_action_ordinal) + ("closest_unnotified_ancestor_action_ordinal", a.closest_unnotified_ancestor_action_ordinal) + ("recv_sequence", a.recv_sequence) + ("auth_sequence", a.auth_sequence) + ("code_sequence", a.code_sequence) + ("abi_sequence", a.abi_sequence); + + fc::variants deltas; + deltas.reserve(a.account_ram_deltas.size()); + for (const auto& d : a.account_ram_deltas) { + deltas.emplace_back(fc::mutable_variant_object() + ("account", d.account.to_string()) + ("delta", d.delta)); + } + v("account_ram_deltas", std::move(deltas)); + + if (a.cpu_usage_us.has_value()) + v("cpu_usage_us", *a.cpu_usage_us); + if (a.net_usage.has_value()) + v("net_usage", *a.net_usage); + } + + if (!decoded.params.is_null()) + v("params", decoded.params); + if (decoded.return_data.has_value()) + v("return_data", *decoded.return_data); + if (!decoded.error_message.empty()) + v("decode_error", decoded.error_message); + + return v; +} + +} // namespace sysio::trace_api + namespace { using namespace sysio::trace_api; @@ -14,57 +65,25 @@ namespace { fc::variants process_actions(const std::vector& actions, const data_handler_function& data_handler) { fc::variants result; result.reserve(actions.size()); - std::vector indices(actions.size()); - std::iota(indices.begin(), indices.end(), 0); - std::sort(indices.begin(), indices.end(), [&actions](const int& lhs, const int& rhs) -> bool { - return actions.at(lhs).global_sequence < actions.at(rhs).global_sequence; + // global_sequence is unique per action (chain invariant), so sort stability + // is not required. + std::vector sorted; + sorted.reserve(actions.size()); + for (const auto& a : actions) sorted.push_back(&a); + std::sort(sorted.begin(), sorted.end(), [](const auto* l, const auto* r){ + return l->global_sequence < r->global_sequence; }); - for ( int index : indices) { - const auto& a = actions.at(index); - auto action_variant = fc::mutable_variant_object(); - - action_variant - ("action_ordinal", a.action_ordinal) - ("creator_action_ordinal", a.creator_action_ordinal) - ("closest_unnotified_ancestor_action_ordinal", a.closest_unnotified_ancestor_action_ordinal) - ("global_sequence", a.global_sequence) - ("recv_sequence", a.recv_sequence) - ("auth_sequence", a.auth_sequence) - ("code_sequence", a.code_sequence) - ("abi_sequence", a.abi_sequence) - ("receiver", a.receiver.to_string()) - ("account", a.account.to_string()) - ("name", a.action.to_string()) - ("authorization", serialize_authorizations(a.authorization)) - ("data", fc::to_hex(a.data.data(), a.data.size())) - ("return_value", fc::to_hex(a.return_value.data(), a.return_value.size())); - - // account_ram_deltas - { - fc::variants deltas; - deltas.reserve(a.account_ram_deltas.size()); - for (const auto& d : a.account_ram_deltas) { - deltas.emplace_back(fc::mutable_variant_object() - ("account", d.account.to_string()) - ("delta", d.delta) - ); - } - action_variant("account_ram_deltas", std::move(deltas)); - } - - if (a.cpu_usage_us.has_value()) - action_variant("cpu_usage_us", *a.cpu_usage_us); - if (a.net_usage.has_value()) - action_variant("net_usage", *a.net_usage); + for (const action_trace_v0* ap : sorted) { + const auto& a = *ap; auto [params, return_data] = data_handler(a); - if (!params.is_null()) { - action_variant("params", params); - } - if(return_data.has_value()){ - action_variant("return_data", *return_data); - } + decoded_action decoded{std::move(params), std::move(return_data), {}}; + // legacy process_block path used serialize_to_variant's tuple which doesn't + // convey decode_error, so leave the field empty here; callers that want the + // error path should go through get_actions instead. + fc::mutable_variant_object action_variant = build_action_variant(a, decoded, variant_shape::full); + // block-trace-local fields (populated by the enclosing transaction, not here) result.emplace_back( std::move(action_variant) ); } return result; diff --git a/plugins/trace_api_plugin/src/store_provider.cpp b/plugins/trace_api_plugin/src/store_provider.cpp index 0a391bf7f6..68d3b84215 100644 --- a/plugins/trace_api_plugin/src/store_provider.cpp +++ b/plugins/trace_api_plugin/src/store_provider.cpp @@ -142,10 +142,42 @@ namespace sysio::trace_api { if (slice_number) { if (auto reader = _slice_directory.find_trx_id_index_slice(*slice_number)) { if (auto block_num = reader->lookup(trx_id)) { - trx_block_nums.insert(*block_num); - return false; // found in an irreversible slice; stop + // Confirm the hit by scanning the candidate block's + // block_trxs_entry for a full trx_id match. A naked 64-bit + // prefix collision (extremely rare with natural sha256 ids, + // but findable in ~2^32 GPU work adversarially) would otherwise + // return the wrong block_num and the downstream get_block + // scan would 404 the real trx. On confirm failure, reset the + // file to offset 0 and fall through to the linear scan below. + bool confirmed = false; + { + metadata_log_entry entry; + auto ds = trx_id_file.create_datastream(); + const uint64_t end = file_size(trx_id_file.get_file_path()); + while (trx_id_file.tellp() < end) { + yield(); + fc::raw::unpack(ds, entry); + if (!std::holds_alternative(entry)) continue; + const auto& te = std::get(entry); + if (te.block_num != *block_num) continue; + for (const auto& id : te.ids) { + if (id == trx_id) { confirmed = true; break; } + } + if (confirmed) break; + } + } + if (confirmed) { + trx_block_nums.insert(*block_num); + return false; // found in an irreversible slice; stop + } + // Prefix collision: reset the file so the linear scan starts + // from offset 0 -- the target trx may still be in a DIFFERENT + // block whose trxs entry we passed over during confirmation. + trx_id_file.seek(0); + // fall through to linear scan + } else { + return true; // not in this indexed slice; continue to next } - return true; // not in this indexed slice; continue to next } } @@ -404,35 +436,47 @@ namespace sysio::trace_api { } } // anonymous namespace - std::optional slice_directory::first_recorded_block() const { - for (const auto& path : collect_index_paths(_slice_dir, /*ascending=*/true)) { - if (const auto r = scan_index_slice(path, _current_version)) - return r->first; + std::optional> slice_directory::first_and_last_recorded_blocks() const { + // Named local with the exact return type so the compiler can NRVO it directly + // into the caller's slot. Single directory scan: collect ascending paths, + // walk from both ends. Returning both bounds from one call guarantees callers + // see a consistent view -- either both values are present or neither is. + std::optional> result; + + const auto paths = collect_index_paths(_slice_dir, /*ascending=*/true); + std::optional first_block; + for (const auto& path : paths) { + if (const auto r = scan_index_slice(path, _current_version)) { + first_block = r->first; + break; + } } - return std::nullopt; - } + if (!first_block) + return result; - std::optional slice_directory::last_recorded_block() const { - for (const auto& path : collect_index_paths(_slice_dir, /*ascending=*/false)) { - if (const auto r = scan_index_slice(path, _current_version)) - return r->second; + // first_block was found, so at least one slice has block entries; the reverse + // pass is guaranteed to find at least one (worst case, the same slice). + uint32_t last_block = *first_block; + for (auto it = paths.rbegin(); it != paths.rend(); ++it) { + if (const auto r = scan_index_slice(*it, _current_version)) { + last_block = r->second; + break; + } } - return std::nullopt; - } - std::optional store_provider::first_recorded_block() const { - return _slice_directory.first_recorded_block(); + result.emplace(*first_block, last_block); + return result; } - std::optional store_provider::last_recorded_block() const { - return _slice_directory.last_recorded_block(); + std::optional> store_provider::first_and_last_recorded_blocks() const { + return _slice_directory.first_and_last_recorded_blocks(); } void store_provider::append_abi(chain::name account, uint64_t global_seq, std::vector abi_bytes) { _abi_log.append(account, global_seq, std::move(abi_bytes)); } - std::optional> store_provider::lookup_abi(chain::name account, uint64_t global_seq) const { + std::optional store_provider::lookup_abi(chain::name account, uint64_t global_seq) const { return _abi_log.lookup(account, global_seq); } diff --git a/plugins/trace_api_plugin/src/trace_api_plugin.cpp b/plugins/trace_api_plugin/src/trace_api_plugin.cpp index 4ecdea157c..64df50bc40 100644 --- a/plugins/trace_api_plugin/src/trace_api_plugin.cpp +++ b/plugins/trace_api_plugin/src/trace_api_plugin.cpp @@ -14,6 +14,7 @@ using namespace sysio::trace_api; using namespace sysio::trace_api::configuration_utils; +using namespace sysio::chain::literals; using boost::signals2::scoped_connection; namespace { @@ -89,19 +90,15 @@ namespace { store->append_trx_ids(std::move(tt)); } - std::optional first_recorded_block() const { - return store->first_recorded_block(); - } - - std::optional last_recorded_block() const { - return store->last_recorded_block(); + std::optional> first_and_last_recorded_blocks() const { + return store->first_and_last_recorded_blocks(); } void append_abi(chain::name account, uint64_t global_seq, std::vector abi_bytes) { store->append_abi(account, global_seq, std::move(abi_bytes)); } - std::optional> lookup_abi(chain::name account, uint64_t global_seq) const { + std::optional lookup_abi(chain::name account, uint64_t global_seq) const { return store->lookup_abi(account, global_seq); } @@ -133,11 +130,11 @@ struct trace_api_common_impl { cfg_options("trace-minimum-uncompressed-irreversible-history-blocks", boost::program_options::value()->default_value(-1), "Number of blocks to ensure are uncompressed past LIB. Compressed \"slice\" files are still accessible but may carry a performance loss on retrieval\n" "A value of -1 indicates that automatic compression of \"slice\" files will be turned off."); - cfg_options("trace-max-block-range", bpo::value()->default_value(100), + cfg_options("trace-max-block-range", bpo::value()->default_value(1000), "Maximum number of blocks scanned by a single get_actions or get_token_transfers request.\n" - "block_num_end is silently clamped to block_num_start + this - 1.\n" - "Clients paginate by advancing block_num_start by this amount each call.\n" - "A value of -1 removes the cap (use with caution on public nodes)."); + "Must be in [1, 10000]. block_num_end is silently clamped to block_num_start + this - 1.\n" + "Clients paginate by advancing block_num_start by this amount each call. The response\n" + "envelope reports the actual range scanned."); } void plugin_initialize(const appbase::variables_map& options) { @@ -168,11 +165,10 @@ struct trace_api_common_impl { minimum_uncompressed_irreversible_history_blocks = uncompressed_blocks; } - const int32_t block_range = options.at("trace-max-block-range").as(); - SYS_ASSERT(block_range == -1 || block_range > 0, chain::plugin_config_exception, - "\"trace-max-block-range\" must be -1 (unlimited) or a positive value."); - max_block_range = (block_range == -1) ? std::numeric_limits::max() - : static_cast(block_range); + const uint32_t block_range = options.at("trace-max-block-range").as(); + SYS_ASSERT(block_range >= 1 && block_range <= 10'000, chain::plugin_config_exception, + "\"trace-max-block-range\" must be in [1, 10000]; got {}", block_range); + max_block_range = block_range; store = std::make_shared( trace_dir, @@ -215,20 +211,22 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_this& common ) :common(common) {} - static void set_program_options(appbase::options_description&, appbase::options_description&) {} - void plugin_initialize(const appbase::variables_map&) { - fc_ilog(_log, "initializing trace api rpc plugin"); + fc_ilog(_log, "trace_api: initializing trace api rpc plugin"); max_block_range = common->max_block_range; auto store = common->store; auto data_handler = std::make_shared( [](const exception_with_context& e) { - // Log at debug and fall back to raw hex — do not rethrow, since + // Log at debug and fall back to raw hex -- do not rethrow, since // ABI capture is automatic and decoding failures should be soft. log_exception(e, fc::log_level::debug); }, - [store](chain::name account, uint64_t global_seq) { - return store->lookup_abi(account, global_seq); + [store](chain::name account, uint64_t global_seq) -> std::optional { + std::optional out; + if (auto r = store->lookup_abi(account, global_seq)) { + out.emplace(abi_data_handler::lookup_entry{r->effective_global_seq, std::move(r->abi_bytes)}); + } + return out; } ); @@ -337,6 +335,7 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_this(); if (obj.contains("block_num_end")) query.block_num_end = obj["block_num_end"].as(); + if (obj.contains("include_notifications")) + include_notifications = obj["include_notifications"].as_bool(); } catch (...) { error_results results{400, "Bad request body"}; cb( 400, fc::variant( results )); @@ -358,6 +359,16 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_this query.block_num_end) { error_results results{400, "block_num_start must be <= block_num_end"}; cb( 400, fc::variant( results )); @@ -368,7 +379,10 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_thisget_actions(query); - cb( 200, fc::mutable_variant_object()("actions", result.actions) ); + cb( 200, fc::mutable_variant_object() + ("block_num_start", query.block_num_start) + ("block_num_end", query.block_num_end) + ("actions", result.actions) ); } catch (...) { http_plugin::handle_exception("trace_api", "get_actions", body, cb); } @@ -378,21 +392,24 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_this(); if (obj.contains("block_num_end")) @@ -402,9 +419,6 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_this query.block_num_end) { @@ -417,7 +431,10 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_thisget_token_transfer_actions(query); - cb( 200, fc::mutable_variant_object()("transfers", result.actions) ); + cb( 200, fc::mutable_variant_object() + ("block_num_start", query.block_num_start) + ("block_num_end", query.block_num_end) + ("transfers", result.actions) ); } catch (...) { http_plugin::handle_exception("trace_api", "get_token_transfers", body, cb); } @@ -428,9 +445,9 @@ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_this::max()) return; const uint64_t max_end = uint64_t{query.block_num_start} + max_block_range - 1; if (max_end < query.block_num_end) query.block_num_end = static_cast(max_end); @@ -448,7 +465,7 @@ struct trace_api_plugin_impl { :common(common) {} void plugin_initialize(const appbase::variables_map& options) { - fc_ilog(_log, "initializing trace api plugin"); + fc_ilog(_log, "trace_api: initializing trace api plugin"); auto log_exceptions_and_shutdown = [](const exception_with_context& e) { log_exception(e, fc::log_level::error); app().quit(); @@ -465,7 +482,11 @@ struct trace_api_plugin_impl { const auto* meta = chain.find_account_metadata(account); if (meta && meta->abi.size() > 0) result.emplace(meta->abi.data(), meta->abi.data() + meta->abi.size()); - } catch (...) {} + } catch (const std::exception& e) { + fc_dlog(_log, "trace_api: lazy ABI fetch for {} failed: {}", account, e.what()); + } catch (...) { + fc_dlog(_log, "trace_api: lazy ABI fetch for {} failed: unknown", account); + } return result; }; @@ -531,7 +552,6 @@ trace_api_plugin::~trace_api_plugin() = default; void trace_api_plugin::set_program_options(appbase::options_description& cli, appbase::options_description& cfg) { trace_api_common_impl::set_program_options(cli, cfg); - trace_api_rpc_plugin_impl::set_program_options(cli, cfg); } void trace_api_plugin::plugin_initialize(const appbase::variables_map& options) { @@ -568,7 +588,6 @@ trace_api_rpc_plugin::~trace_api_rpc_plugin() = default; void trace_api_rpc_plugin::set_program_options(appbase::options_description& cli, appbase::options_description& cfg) { trace_api_common_impl::set_program_options(cli, cfg); - trace_api_rpc_plugin_impl::set_program_options(cli, cfg); } void trace_api_rpc_plugin::plugin_initialize(const appbase::variables_map& options) { diff --git a/plugins/trace_api_plugin/src/trx_id_index.cpp b/plugins/trace_api_plugin/src/trx_id_index.cpp index 9c80e9cf85..ff28a909d7 100644 --- a/plugins/trace_api_plugin/src/trx_id_index.cpp +++ b/plugins/trace_api_plugin/src/trx_id_index.cpp @@ -2,10 +2,13 @@ #include #include +#include + #include #include #include +#include namespace sysio::trace_api { @@ -24,6 +27,11 @@ void trx_id_index_writer::add(const chain::transaction_id_type& trx_id, uint32_t void trx_id_index_writer::write(const std::filesystem::path& path) const { // Target load factor <= 0.5; bucket_count is a power of two for fast modulo. + // Guard the uint32_t cast: in practice a slice holds at most ~10M trxs, + // but a future regression could exceed UINT32_MAX and silently truncate. + SYS_ASSERT(_entries.size() <= std::numeric_limits::max() / 2 - 1, + chain::plugin_exception, + "trx_id_index entry count {} exceeds uint32 range", _entries.size()); const uint32_t n = static_cast(_entries.size()); const uint32_t bucket_count = std::max(4u, std::bit_ceil(n * 2 + 1)); const uint32_t mask = bucket_count - 1; diff --git a/plugins/trace_api_plugin/test/test_abi_log.cpp b/plugins/trace_api_plugin/test/test_abi_log.cpp index c38594cf95..663c3826b1 100644 --- a/plugins/trace_api_plugin/test/test_abi_log.cpp +++ b/plugins/trace_api_plugin/test/test_abi_log.cpp @@ -56,7 +56,7 @@ BOOST_FIXTURE_TEST_CASE(single_entry_round_trip, abi_log_fixture) { auto result = log.lookup("sysio.token"_n, 100); BOOST_REQUIRE(result.has_value()); - BOOST_CHECK_EQUAL_COLLECTIONS(result->begin(), result->end(), blob.begin(), blob.end()); + BOOST_CHECK_EQUAL_COLLECTIONS(result->abi_bytes.begin(), result->abi_bytes.end(), blob.begin(), blob.end()); } BOOST_FIXTURE_TEST_CASE(multiple_accounts_round_trip, abi_log_fixture) { @@ -68,11 +68,11 @@ BOOST_FIXTURE_TEST_CASE(multiple_accounts_round_trip, abi_log_fixture) { auto r1 = log.lookup("sysio.token"_n, 50); BOOST_REQUIRE(r1.has_value()); - BOOST_CHECK_EQUAL_COLLECTIONS(r1->begin(), r1->end(), blob1.begin(), blob1.end()); + BOOST_CHECK_EQUAL_COLLECTIONS(r1->abi_bytes.begin(), r1->abi_bytes.end(), blob1.begin(), blob1.end()); auto r2 = log.lookup("sysio"_n, 200); BOOST_REQUIRE(r2.has_value()); - BOOST_CHECK_EQUAL_COLLECTIONS(r2->begin(), r2->end(), blob2.begin(), blob2.end()); + BOOST_CHECK_EQUAL_COLLECTIONS(r2->abi_bytes.begin(), r2->abi_bytes.end(), blob2.begin(), blob2.end()); } // --------------------------------------------------------------------------- @@ -95,22 +95,22 @@ BOOST_FIXTURE_TEST_CASE(returns_abi_in_effect_at_global_seq, abi_log_fixture) { // Exactly at v1 auto at100 = log.lookup("sysio.token"_n, 100); BOOST_REQUIRE(at100.has_value()); - BOOST_CHECK_EQUAL_COLLECTIONS(at100->begin(), at100->end(), v1.begin(), v1.end()); + BOOST_CHECK_EQUAL_COLLECTIONS(at100->abi_bytes.begin(), at100->abi_bytes.end(), v1.begin(), v1.end()); // Between v1 and v2 -> v1 auto at150 = log.lookup("sysio.token"_n, 150); BOOST_REQUIRE(at150.has_value()); - BOOST_CHECK_EQUAL_COLLECTIONS(at150->begin(), at150->end(), v1.begin(), v1.end()); + BOOST_CHECK_EQUAL_COLLECTIONS(at150->abi_bytes.begin(), at150->abi_bytes.end(), v1.begin(), v1.end()); // Exactly at v2 auto at200 = log.lookup("sysio.token"_n, 200); BOOST_REQUIRE(at200.has_value()); - BOOST_CHECK_EQUAL_COLLECTIONS(at200->begin(), at200->end(), v2.begin(), v2.end()); + BOOST_CHECK_EQUAL_COLLECTIONS(at200->abi_bytes.begin(), at200->abi_bytes.end(), v2.begin(), v2.end()); // After v3 auto at500 = log.lookup("sysio.token"_n, 500); BOOST_REQUIRE(at500.has_value()); - BOOST_CHECK_EQUAL_COLLECTIONS(at500->begin(), at500->end(), v3.begin(), v3.end()); + BOOST_CHECK_EQUAL_COLLECTIONS(at500->abi_bytes.begin(), at500->abi_bytes.end(), v3.begin(), v3.end()); } BOOST_FIXTURE_TEST_CASE(lookup_wrong_account_returns_nullopt, abi_log_fixture) { @@ -126,7 +126,7 @@ BOOST_FIXTURE_TEST_CASE(accounts_do_not_bleed_into_each_other, abi_log_fixture) auto token_result = log.lookup("sysio.token"_n, 250); BOOST_REQUIRE(token_result.has_value()); - BOOST_CHECK(*token_result == make_abi("token-100")); + BOOST_CHECK(token_result->abi_bytes == make_abi("token-100")); BOOST_CHECK(!log.lookup("sysio.token"_n, 50)); } @@ -137,7 +137,7 @@ BOOST_FIXTURE_TEST_CASE(empty_blob_round_trip, abi_log_fixture) { auto result = log.lookup("clearme"_n, 999); BOOST_REQUIRE(result.has_value()); - BOOST_CHECK(result->empty()); + BOOST_CHECK(result->abi_bytes.empty()); } BOOST_FIXTURE_TEST_CASE(last_write_wins_for_duplicate_key, abi_log_fixture) { @@ -147,7 +147,7 @@ BOOST_FIXTURE_TEST_CASE(last_write_wins_for_duplicate_key, abi_log_fixture) { auto result = log.lookup("acct"_n, 100); BOOST_REQUIRE(result.has_value()); - BOOST_CHECK(*result == make_abi("second")); + BOOST_CHECK(result->abi_bytes == make_abi("second")); } // --------------------------------------------------------------------------- @@ -168,17 +168,17 @@ BOOST_FIXTURE_TEST_CASE(append_after_restart, abi_log_fixture) { auto r1 = log.lookup("sysio.token"_n, 100); BOOST_REQUIRE(r1.has_value()); - BOOST_CHECK(*r1 == make_abi("token-100")); + BOOST_CHECK(r1->abi_bytes == make_abi("token-100")); auto r2 = log.lookup("sysio"_n, 200); BOOST_REQUIRE(r2.has_value()); - BOOST_CHECK(*r2 == make_abi("sysio-200")); + BOOST_CHECK(r2->abi_bytes == make_abi("sysio-200")); log.append("newacct"_n, 300, make_abi("new-300")); auto r3 = log.lookup("newacct"_n, 300); BOOST_REQUIRE(r3.has_value()); - BOOST_CHECK(*r3 == make_abi("new-300")); + BOOST_CHECK(r3->abi_bytes == make_abi("new-300")); } // --------------------------------------------------------------------------- @@ -240,7 +240,7 @@ BOOST_FIXTURE_TEST_CASE(truncated_tail_is_recovered, abi_log_fixture) { log.append("d"_n, 400, make_abi("d-400")); auto r = log.lookup("d"_n, 400); BOOST_REQUIRE(r.has_value()); - BOOST_CHECK(*r == make_abi("d-400")); + BOOST_CHECK(r->abi_bytes == make_abi("d-400")); } // Flip a byte inside record-b's blob so its CRC fails. Recovery truncates @@ -277,7 +277,7 @@ BOOST_FIXTURE_TEST_CASE(crc_mismatch_drops_record_and_tail, abi_log_fixture) { log.append("d"_n, 400, make_abi("d-blob")); auto r = log.lookup("d"_n, 400); BOOST_REQUIRE(r.has_value()); - BOOST_CHECK(*r == make_abi("d-blob")); + BOOST_CHECK(r->abi_bytes == make_abi("d-blob")); } // --------------------------------------------------------------------------- @@ -306,7 +306,7 @@ BOOST_FIXTURE_TEST_CASE(many_accounts_many_versions, abi_log_fixture) { auto result = log.lookup(acct, seq); BOOST_REQUIRE_MESSAGE(result.has_value(), "missing entry for account " << a << " version " << v); auto expected = make_abi("a" + std::to_string(a) + "v" + std::to_string(v)); - BOOST_CHECK_EQUAL_COLLECTIONS(result->begin(), result->end(), expected.begin(), expected.end()); + BOOST_CHECK_EQUAL_COLLECTIONS(result->abi_bytes.begin(), result->abi_bytes.end(), expected.begin(), expected.end()); } } diff --git a/plugins/trace_api_plugin/test/test_continuity.cpp b/plugins/trace_api_plugin/test/test_continuity.cpp index d639ca5cbf..06d946d8dc 100644 --- a/plugins/trace_api_plugin/test/test_continuity.cpp +++ b/plugins/trace_api_plugin/test/test_continuity.cpp @@ -19,15 +19,22 @@ struct continuity_mock_store { void append_lib(uint32_t) {} void append_trx_ids(block_trxs_entry) {} - std::optional first_recorded_block() const { return _first_block; } - std::optional last_recorded_block() const { return _last_block; } + std::optional> first_and_last_recorded_blocks() const { + if (!_first_block) return std::nullopt; + return std::make_pair(*_first_block, _last_block.value_or(*_first_block)); + } std::optional _first_block; std::optional _last_block; }; struct continuity_fixture { - // Convenience: data [first, last] exists, or nullopt for empty store + // Convenience: data [first, last] exists, or nullopt for empty store. + // Each try_block_start() constructs a fresh extractor instance -- models + // an independent node startup. For tests that need to assert the check + // fires only once across multiple block_start signals on the SAME + // extractor, do not use this fixture; build the extractor inline (see + // check_only_on_first_block_start below). continuity_fixture(std::optional first_block, std::optional last_block) : store_first(first_block), store_last(last_block) {} @@ -136,4 +143,26 @@ BOOST_AUTO_TEST_SUITE(continuity_tests) BOOST_CHECK(!threw); } + // The continuity check flips its "already checked" flag BEFORE running, so + // subsequent block_start signals skip the check regardless of what the + // except_handler did on the first one. Verify via a non-throwing handler + // (which would otherwise re-enter the check if the flag weren't set early). + BOOST_AUTO_TEST_CASE(check_not_rerun_after_non_throwing_except_handler) { + int handler_calls = 0; + auto except = exception_handler{[&handler_calls](const exception_with_context&) { + ++handler_calls; + // deliberately do NOT throw -- simulates a handler that just logs + }}; + + // Prior data ending at 100; first block_start at 200 is a forward gap. + chain_extraction_impl_type impl( + continuity_mock_store{1, 100}, std::move(except)); + + impl.signal_block_start(200); // gap detected; handler called once + BOOST_CHECK_EQUAL(handler_calls, 1); + + impl.signal_block_start(300); // second call must NOT re-invoke the check + BOOST_CHECK_EQUAL(handler_calls, 1); + } + BOOST_AUTO_TEST_SUITE_END() diff --git a/plugins/trace_api_plugin/test/test_data_handlers.cpp b/plugins/trace_api_plugin/test/test_data_handlers.cpp index aa2dea8281..7d1ea665e3 100644 --- a/plugins/trace_api_plugin/test/test_data_handlers.cpp +++ b/plugins/trace_api_plugin/test/test_data_handlers.cpp @@ -15,10 +15,12 @@ namespace { return fc::raw::pack(abi); } - // Build a lookup_fn that returns packed ABI bytes for a given account + // Build a lookup_fn that returns packed ABI bytes for a given account. + // effective_global_seq is fixed at 0 for test purposes -- the handler's + // cache key becomes (account, 0) regardless of the action's global_seq. abi_data_handler::abi_lookup_fn make_lookup(chain::name account, std::vector abi_bytes) { - return [account, bytes = std::move(abi_bytes)](chain::name a, uint64_t) -> std::optional> { - if (a == account) return bytes; + return [account, bytes = std::move(abi_bytes)](chain::name a, uint64_t) -> std::optional { + if (a == account) return abi_data_handler::lookup_entry{0, bytes}; return std::nullopt; }; } diff --git a/plugins/trace_api_plugin/test/test_extraction.cpp b/plugins/trace_api_plugin/test/test_extraction.cpp index a4ea309869..149c7f9956 100644 --- a/plugins/trace_api_plugin/test/test_extraction.cpp +++ b/plugins/trace_api_plugin/test/test_extraction.cpp @@ -127,11 +127,7 @@ struct extraction_test_fixture { fixture.id_log[tt.block_num] = tt.ids; } - std::optional first_recorded_block() const { - return std::nullopt; // no prior data in unit tests - } - - std::optional last_recorded_block() const { + std::optional> first_and_last_recorded_blocks() const { return std::nullopt; // no prior data in unit tests } @@ -398,8 +394,7 @@ struct abi_capture_fixture { template void append(const BT&) {} void append_lib(uint32_t) {} void append_trx_ids(const block_trxs_entry&) {} - std::optional first_recorded_block() const { return std::nullopt; } - std::optional last_recorded_block() const { return std::nullopt; } + std::optional> first_and_last_recorded_blocks() const { return std::nullopt; } void append_abi(chain::name account, uint64_t global_seq, std::vector abi_bytes) { fixture.abi_calls.push_back({account, global_seq, std::move(abi_bytes)}); diff --git a/plugins/trace_api_plugin/test/test_get_actions.cpp b/plugins/trace_api_plugin/test/test_get_actions.cpp index 7619fdfadd..8c4027638e 100644 --- a/plugins/trace_api_plugin/test/test_get_actions.cpp +++ b/plugins/trace_api_plugin/test/test_get_actions.cpp @@ -1,5 +1,6 @@ #include +#include #include #include @@ -33,6 +34,21 @@ struct get_actions_fixture { return fixture.mock_data_handler(a); } + // Production shared_provider exposes decode(); the request_handler's + // get_actions path calls decode() directly so a decode_error field can + // be surfaced on failure. The mock builds a decode_result from the + // tuple returned by mock_data_handler -- decode never fails in tests. + abi_data_handler::decode_result decode(const action_trace_v0& a) { + auto [params, return_data] = fixture.mock_data_handler(a); + abi_data_handler::decode_result r; + r.params = std::move(params); + r.return_data = std::move(return_data); + r.status = r.params.is_null() + ? abi_data_handler::decode_status::not_attempted + : abi_data_handler::decode_status::ok; + return r; + } + get_actions_fixture& fixture; }; diff --git a/plugins/trace_api_plugin/test/test_trx_id_index.cpp b/plugins/trace_api_plugin/test/test_trx_id_index.cpp index ab742b45cd..92ea9eca4a 100644 --- a/plugins/trace_api_plugin/test/test_trx_id_index.cpp +++ b/plugins/trace_api_plugin/test/test_trx_id_index.cpp @@ -18,7 +18,7 @@ chain::transaction_id_type make_trx_id(uint64_t prefix64, uint8_t disambiguator chain::transaction_id_type id; std::memcpy(id.data(), &prefix64, sizeof(prefix64)); // vary the last byte so ids with the same prefix64 are still distinct objects - id.data()[31] = disambiguator; + id.data()[id.data_size() - 1] = disambiguator; return id; } diff --git a/plugins/trace_api_plugin/trace_api_plugin.md b/plugins/trace_api_plugin/trace_api_plugin.md index a61884b98a..a345ddf991 100644 --- a/plugins/trace_api_plugin/trace_api_plugin.md +++ b/plugins/trace_api_plugin/trace_api_plugin.md @@ -10,23 +10,23 @@ token transfers. ## Table of contents 1. [Overview](#overview) -2. [Enabling the plugin](#enabling-the-plugin) +2. [Quick start](#quick-start) 3. [Configuration options](#configuration-options) -4. [On-disk layout](#on-disk-layout) - - [Slice files](#slice-files) - - [Transaction-id index](#transaction-id-index) - - [ABI store](#abi-store) -5. [Startup continuity check](#startup-continuity-check) -6. [ABI decoding](#abi-decoding) -7. [HTTP API reference](#http-api-reference) +4. [HTTP API reference](#http-api-reference) - [get_block](#get_block) - [get_transaction_trace](#get_transaction_trace) - [get_actions](#get_actions) - [get_token_transfers](#get_token_transfers) -8. [Pagination guide](#pagination-guide) -9. [Exchange / indexer integration guide](#exchange--indexer-integration-guide) -10. [Maintenance and retention](#maintenance-and-retention) -11. [Plugin variants](#plugin-variants) +5. [Pagination guide](#pagination-guide) +6. [Exchange / indexer integration guide](#exchange--indexer-integration-guide) +7. [ABI decoding](#abi-decoding) +8. [Operations](#operations) + - [Maintenance and retention](#maintenance-and-retention) + - [Startup continuity check](#startup-continuity-check) + - [Plugin variants](#plugin-variants) +9. [Implementation details](#implementation-details) + - [On-disk layout](#on-disk-layout) + - [ABI capture mechanics](#abi-capture-mechanics) --- @@ -46,15 +46,15 @@ Key design points: action_traces` is stored, so inline notifications are captured alongside the originating action. - **Versioned ABI decoding** — the ABI in effect at the moment each - `setcode`/`setabi` transaction was applied is captured in `abi_log.log`. - Queries decode `data` and `return_value` fields using the historically - correct ABI, not the current on-chain ABI. + `setabi` transaction was applied is captured in `abi_log.log`. Queries + decode `data` and `return_value` fields using the historically correct + ABI, not the current on-chain ABI. - **O(1) transaction lookup** — a per-slice hash index maps `trx_id` to `block_num` so `get_transaction_trace` does not scan the chain. --- -## Enabling the plugin +## Quick start Add to `config.ini` or pass on the command line: @@ -82,7 +82,7 @@ default). | `trace-slice-stride` | `10000` | Number of blocks per slice file. Must be in `[1, 1000000]`. Larger values reduce file count but bloat the block-offset sidecar's per-slice pre-allocation (`stride * 8` bytes, sparse) and stress the per-slice trx_id hash index (rejected if it would need more than 2^28 buckets). | | `trace-minimum-irreversible-history-blocks` | `-1` | Blocks past LIB to retain before old slices can be auto-deleted. `-1` disables automatic deletion (keep forever). | | `trace-minimum-uncompressed-irreversible-history-blocks` | `-1` | Blocks past LIB to keep uncompressed. Slices older than this threshold are transparently compressed. `-1` disables automatic compression. | -| `trace-max-block-range` | `100` | Maximum number of blocks scanned by a single `get_actions` or `get_token_transfers` request. `block_num_end` is silently clamped to `block_num_start + trace-max-block-range - 1`. Clients paginate by advancing `block_num_start` by this amount on each call. Set to `-1` to remove the cap entirely. | +| `trace-max-block-range` | `1000` | Maximum number of blocks scanned by a single `get_actions` or `get_token_transfers` request. Must be in `[1, 10000]`. `block_num_end` is silently clamped to `block_num_start + trace-max-block-range - 1` when a request asks for more. The response envelope always reports the actual range scanned. | ### Recommended production settings @@ -94,192 +94,17 @@ trace-slice-stride = 10000 trace-minimum-uncompressed-irreversible-history-blocks = 28000000 # Keep 1 year total (compressed) trace-minimum-irreversible-history-blocks = 365000000 +# Widen per-request scan window for private/trusted nodes +trace-max-block-range = 5000 ``` For a full-history archive node omit or set both retention options to `-1`. --- -## On-disk layout - -All files live inside `trace-dir`. The directory is monitored by -`resource_monitor_plugin` when that plugin is loaded. - -### Slice files - -Blocks are grouped into contiguous slices of `trace-slice-stride` blocks -each. Each slice is represented by four files that share a common range -suffix `-` (zero-padded to 10 digits): - -| File | Description | -|------|-------------| -| `trace_-.log` | Serialized `block_trace_v0` records (action data). | -| `trace_index_-.log` | Append-only metadata log of `block_entry_v0` and `lib_entry_v0` records. Source of truth; used as a fallback for `get_block` and to track LIB advancement within the slice. | -| `trace_blk_idx_-.log` | Block-offset sidecar (see below). Enables O(1) `get_block` lookups regardless of the block's position within the slice. | -| `trace_trx_idx_-.log` | Transaction-id hash index (see below). | - -When a slice is compressed the trace file is replaced by: - -| File | Description | -|------|-------------| -| `trace_-.clog` | zlib-compressed trace data with embedded seek points for random access. | - -The index and trx_id index files are not compressed. - -**Example** (10 000-block stride, blocks 0–29 999): - -``` -traces/ - trace_0000000000-0000010000.log - trace_index_0000000000-0000010000.log - trace_blk_idx_0000000000-0000010000.log - trace_trx_idx_0000000000-0000010000.log - trace_0000010000-0000020000.log - trace_index_0000010000-0000020000.log - trace_blk_idx_0000010000-0000020000.log - trace_trx_idx_0000010000-0000020000.log - trace_0000020000-0000030000.clog <- compressed - trace_index_0000020000-0000030000.log - trace_blk_idx_0000020000-0000030000.log - trace_trx_idx_0000020000-0000030000.log - abi_log.log -``` - -### Block-offset index - -`trace_blk_idx_-.log` is a flat fixed-size array of 64-bit -trace-log offsets, one entry per block in the slice, used by -`/v1/trace_api/get_block` for O(1) block lookups. - -- **Header** (16 bytes): magic `BLIX`, version 1, slice width, reserved. -- **Slots** (8 bytes each): `offset + 1` into `trace_-.log`, - or 0 when the slot is empty. The `+1` encoding reserves 0 as an empty - sentinel since a block's trace data can legitimately live at offset 0. - -The sidecar is written synchronously alongside the metadata log as each -block is persisted. Forks that re-apply the same block number overwrite -the slot naturally. If the sidecar is missing or reports an empty slot, -`get_block` falls back to scanning the metadata log. - -### Transaction-id index - -`trace_trx_idx_-.log` is a compact open-addressing hash table -(load factor ≤ 0.5, linear probing) that maps a 64-bit prefix of a -transaction SHA-256 to the block number containing that transaction. - -- **Header** (16 bytes): magic `TRIX`, version 1, bucket_count, reserved. -- **Buckets** (16 bytes each): `prefix64 (u64)` + `block_num (u32)` + - `reserved (u32)`. Empty slots have `block_num == 0`. - -The index is built once per slice when the slice's last block becomes -irreversible. Queries against `/v1/trace_api/get_transaction_trace` use -this index for O(1) `trx_id → block_num` resolution instead of scanning -the chain. - -### ABI log - -`abi_log.log` is an append-only file that persists the ABI published by each -contract account across all `setabi` transactions observed since the node -started (or since the file was first written). - -Format: - -``` -Header (16 bytes): magic "ABIL" (u32), version 1 (u32), reserved (u64) -Records (repeated until EOF): - account (u64) - global_seq (u64) - blob_size (u64) - blob_bytes (blob_size bytes) - crc32 (u32) over (account, global_seq, blob_size, blob_bytes) -``` - -An in-memory index keyed by `(account, global_sequence)` is built at startup -by walking the file record-by-record and validating each CRC. Runtime -lookups go through the index; the matching blob is then read from the file -via `pread()`. Appends stream new records to the end of the file under a -mutex, with no rewrite of existing records. - -Writes are not fsync'd; the on-disk tail may lose the last few records on a -kernel crash. On startup the recovery scan detects torn or CRC-mismatched -records and truncates the file at the first bad one — any lost records are -rebuilt the next time their contract is touched (via observed `setabi` or -the lazy current-ABI fetch). - ---- - -## Startup continuity check - -On the first `block_start` signal after plugin startup the trace store's -recorded block range is compared against the chain's current head, and the -plugin chooses one of four outcomes: - -| Situation | Behavior | -|-----------|----------| -| No prior trace data (empty slice dir) | `info` log, fresh start; tracing begins at the current head. | -| Chain head is within `[first_recorded, last_recorded + 1]` (exact continuation OR overlap from a snapshot replay) | Silent — re-applied blocks naturally overwrite existing slice entries. | -| Chain head is **before** the first recorded block | Plugin throws; `error` log, **node shuts down**. | -| Chain head is **after** `last_recorded + 1` (forward gap) | Plugin throws; `error` log, **node shuts down**. | - -The shutdown is intentional: a gap means the trace store is no longer a -faithful continuous record of chain history, and silently accepting it would -let `get_block` / `get_transaction_trace` return inconsistent data for blocks -on either side of the gap. - -To recover, pick one: - -- **Delete the trace directory and start fresh.** Tracing resumes from the - current chain head; old block traces are lost. -- **Load a snapshot whose chain head is within the existing recorded range - (or one block past it).** Replay covers the existing slice entries; tracing - continues with no gap. -- **Copy the missing slice files from another node** that has the missing - range, then restart. - -The check fires only on the first `block_start` after the plugin loads, so -recovery actions take effect on the next startup. - ---- - -## ABI decoding - -When serving any trace endpoint the plugin attempts to decode the raw `data` -and `return_value` bytes of each action using the ABI captured in -`abi_log.log`. - -- The lookup key is `(account, global_sequence)` — the ABI that was in - effect when that specific action executed is used, not the current ABI. -- If the ABI is unavailable (contract not yet captured, ABI store missing, - or the action predates ABI capture), the fields are returned as raw hex - strings. -- Decoding failures are soft: they are logged at `debug` level and the - response falls back to raw hex instead of returning HTTP 500. This - prevents a malformed ABI in one contract from breaking queries for - unrelated actions in the same block. - -### First-observation + same-trx setabi caveat - -If the plugin observes a contract for the *first* time via a transaction -that *also* contains a `setabi` for that same contract, actions on that -contract which executed *before* the setabi within that transaction cannot -be decoded and are returned as raw hex. The pre-setabi ABI is no longer -reachable from the post-apply chain state, so the plugin deliberately does -not record the post-apply (new) ABI as the contract's pre-observation -baseline — doing so would decode pre-setabi actions with the wrong schema. - -Once the contract has been observed at least once (via any earlier -transaction, or via a setabi-free transaction), later same-trx setabis do -not have this limitation: pre-setabi actions decode correctly with the -previously-recorded ABI. - -When decoded `params` and `return_data` are present they appear alongside the -raw `data` and `return_value` hex fields. - ---- - ## HTTP API reference -All endpoints accept `POST` with a JSON body. The base URL is +All endpoints accept `POST` with a JSON body. The base URL is `/v1/trace_api/`. --- @@ -363,6 +188,7 @@ Retrieve the full action trace for a single block. |------|-----------| | 400 | `block_num` missing or not a number | | 404 | Block not found in trace store | +| 500 | Internal error reading the trace store | --- @@ -382,8 +208,9 @@ The block number is resolved via the per-slice `trx_id` index (O(1)); no block scanning is performed. **Response (200):** A single transaction object in the same shape as one -element of `transactions` in the `get_block` response (includes `id`, -`block_num`, `block_time`, `actions`, `status`, etc.). +element of `transactions` in the `get_block` response (`id`, `block_num`, +`block_time`, `producer_block_id`, `actions`, `cpu_usage_us`, +`net_usage_words`, `signatures`, `transaction_header`). **Error responses:** @@ -391,6 +218,7 @@ element of `transactions` in the `get_block` response (includes `id`, |------|-----------| | 400 | `id` missing or malformed | | 404 | Transaction not found in index, or block trace not found | +| 500 | Internal error reading the trace store | --- @@ -406,15 +234,27 @@ on receiver, account (contract code), and action name. | Field | Type | Default | Description | |-------|------|---------|-------------| | `block_num_start` | uint32 | `0` | First block to scan (inclusive). | -| `block_num_end` | uint32 | `UINT32_MAX` | Last block to scan (inclusive). Silently clamped server-side to `block_num_start + trace-max-block-range - 1`. | -| `receiver` | string | *(any)* | Filter: only actions where `receiver` matches. | -| `account` | string | *(any)* | Filter: only actions where `account` (code account) matches. | -| `action` | string | *(any)* | Filter: only actions where the action name matches. | +| `block_num_end` | uint32 | `UINT32_MAX` | Last block to scan (inclusive). Silently clamped server-side to `block_num_start + trace-max-block-range - 1`. The response reports the actual range scanned. | +| `receiver` | string | *(any)* | Filter: match `act.receiver`. | +| `account` | string | *(any)* | Filter: match `act.account` (the contract whose code ran). | +| `action` | string | *(any)* | Filter: match action name. | +| `include_notifications` | bool | `false` | Notification handling (see below). | + +**Notifications (`include_notifications`):** + +- `false` (default): when exactly one of `receiver` / `account` is + specified, the other is implicitly constrained to the same value — you + get the canonical execution only (`act.account == act.receiver == filter_value`). +- `true`: only the explicitly-specified filters apply. Notifications where + `act.account != act.receiver` are included. + +When both `receiver` and `account` are explicitly specified, the flag has +no effect — both filters apply literally. Returned actions within a transaction are sorted by `global_sequence` (execution order), matching the behavior of `get_block` and chain_plugin's -`push_transaction` response. See the Pagination guide below for the cursor -pattern. +`push_transaction` response. See the [Pagination guide](#pagination-guide) +for the cursor pattern. **Request example:** @@ -431,6 +271,8 @@ pattern. ```json { + "block_num_start": 1, + "block_num_end": 1000, "actions": [ { "action_ordinal": 1, @@ -465,10 +307,16 @@ pattern. } ``` +`block_num_start` and `block_num_end` on the response reflect the actual +range scanned (after clamping), so a client can detect a clamp and +resume pagination from `block_num_end + 1`. + **Response fields:** | Field | Description | |-------|-------------| +| `block_num_start` | First block number actually scanned. | +| `block_num_end` | Last block number actually scanned (after clamping). | | `actions` | Array of matching action objects, ordered by `(block_num, global_sequence)`. | **Action object fields:** @@ -492,8 +340,9 @@ pattern. | `account_ram_deltas` | Array of `{account, delta}` objects capturing RAM allocation changes. | | `cpu_usage_us` | Producer-set CPU in microseconds (present only for input/top-level actions). | | `net_usage` | Producer-set NET usage in bytes (present only for input/top-level actions). | -| `params` | ABI-decoded action payload (omitted when ABI unavailable). | +| `params` | ABI-decoded action payload (omitted when ABI unavailable or decode failed). | | `return_data` | ABI-decoded return value (omitted when ABI unavailable or no return type defined). | +| `decode_error` | Error message; present only when ABI decoding failed and the response falls back to raw hex. | | `trx_id` | ID of the transaction that contains this action. | | `block_num` | Block number. | | `block_time` | Block timestamp (ISO-8601). | @@ -504,6 +353,7 @@ pattern. | Code | Condition | |------|-----------| | 400 | Malformed request body, or `block_num_start > block_num_end`. | +| 500 | Internal error reading the trace store. | #### Receiver vs account @@ -512,28 +362,31 @@ Every SYSIO action has two account fields: - **`account`** — the contract whose code is executed (always the contract that defines the action). - **`receiver`** — the account receiving the notification. For the - originating action `receiver == account`. For inline notifications sent to - other accounts, `receiver != account`. + originating action `receiver == account`. For inline notifications sent + to other accounts, `receiver != account`. -A `sysio.token::transfer` produces two action traces in the store: +A `sysio.token::transfer` from alice to bob produces three action traces +in the store: -| global_seq | receiver | account | -|-----------|----------|---------| -| N | `sysio.token` | `sysio.token` | ← original execution | -| N+1 | `bob` | `sysio.token` | ← inline notification to recipient | +| global_seq | account | receiver | role | +|-----------|---------|----------|------| +| N | `sysio.token` | `sysio.token` | Canonical execution | +| N+1 | `sysio.token` | `alice` | Notification to sender | +| N+2 | `sysio.token` | `bob` | Notification to recipient | -Filter by `receiver="sysio.token"` to get exactly one entry per transfer. -Filter by `account="sysio.token"` to get both. +The default query (no `include_notifications`) implicitly constrains +`receiver == account` when you specify one of them, returning only the +canonical row. To see notifications, set `include_notifications: true`. --- ### get_token_transfers Convenience wrapper around `get_actions` preset to return only token -`transfer` actions for a given contract. Uses -`receiver=account=token_contract, action=transfer` so exactly one entry per -transfer is returned (the canonical execution; inline notification copies -to recipients are excluded). +`transfer` actions for a given contract. Uses +`receiver = account = token_contract, action = "transfer"` so exactly one +entry per transfer is returned (the canonical execution; inline +notifications are excluded). **Endpoint:** `POST /v1/trace_api/get_token_transfers` @@ -543,7 +396,7 @@ to recipients are excluded). |-------|------|---------|-------------| | `token_contract` | string | `sysio.token` | Contract account to filter on. | | `block_num_start` | uint32 | `0` | First block to scan (inclusive). | -| `block_num_end` | uint32 | `UINT32_MAX` | Last block to scan (inclusive). Silently clamped server-side to `block_num_start + trace-max-block-range - 1`. | +| `block_num_end` | uint32 | `UINT32_MAX` | Last block to scan (inclusive). Silently clamped to `block_num_start + trace-max-block-range - 1`. | **Request example:** @@ -559,6 +412,8 @@ to recipients are excluded). ```json { + "block_num_start": 1, + "block_num_end": 1000, "transfers": [ { "global_sequence": 101, @@ -584,54 +439,65 @@ to recipients are excluded). ``` The response uses `"transfers"` as the array key instead of `"actions"`. -`get_token_transfers` returns a **slim subset** of the fields that `get_actions` returns — it omits execution-tree ordinals -(`action_ordinal`, `creator_action_ordinal`, `closest_unnotified_ancestor_action_ordinal`), per-receipt sequence numbers -(`recv_sequence`, `auth_sequence`, `code_sequence`, `abi_sequence`), `account_ram_deltas`, and the resource usage fields -(`cpu_usage_us`, `net_usage`). These are rarely useful for token-transfer exchange/indexer workflows. If you need them, -call `get_actions` with `receiver=account=, action=transfer` instead. +`get_token_transfers` returns a **slim subset** of the fields that +`get_actions` returns — it omits execution-tree ordinals +(`action_ordinal`, `creator_action_ordinal`, +`closest_unnotified_ancestor_action_ordinal`), per-receipt sequence +numbers (`recv_sequence`, `auth_sequence`, `code_sequence`, +`abi_sequence`), `account_ram_deltas`, and the resource usage fields +(`cpu_usage_us`, `net_usage`). These are rarely useful for token-transfer +exchange/indexer workflows. If you need them, call `get_actions` with +`receiver = account = , action = "transfer"` instead. **Error responses:** | Code | Condition | |------|-----------| | 400 | Malformed request body, or `block_num_start > block_num_end`. | +| 500 | Internal error reading the trace store. | --- ## Pagination guide `get_actions` and `get_token_transfers` cap the per-request scan window at -`trace-max-block-range` blocks (default 100). `block_num_end` is silently +`trace-max-block-range` blocks (default 1000). `block_num_end` is silently clamped to `block_num_start + trace-max-block-range - 1` if the client asks -for more. Within that window, ALL matching actions are returned — there -is no per-result limit and no in-window cursor. +for more. The response always includes the actual `block_num_start` and +`block_num_end` scanned so the client can page reliably. + +Within that window, ALL matching actions are returned — there is no +per-result limit and no in-window cursor. -To page across a wide range, advance `block_num_start` by -`trace-max-block-range` on each call: +To page across a wide range, advance `block_num_start` by the response's +`block_num_end + 1` each call: ``` -# Page 1: blocks 1..100 (assuming trace-max-block-range = 100) +# Page 1: blocks 1..1000 (assuming trace-max-block-range = 1000) POST /v1/trace_api/get_actions { "account": "sysio.token", "action": "transfer", "block_num_start": 1, "block_num_end": 1000000 } -# Page 2: blocks 101..200 +# Response: { "block_num_start": 1, "block_num_end": 1000, "actions": [...] } + +# Page 2: blocks 1001..2000 POST /v1/trace_api/get_actions { "account": "sysio.token", "action": "transfer", - "block_num_start": 101, "block_num_end": 1000000 } + "block_num_start": 1001, "block_num_end": 1000000 } ``` -Continue until the response `actions` array is empty AND -`block_num_start > tail of recorded data`. Use the `get_block` endpoint or -out-of-band knowledge of head block to know when to stop. +Continue until `block_num_end` returned by the server equals the +requested `block_num_end` (no clamp happened), or until you catch up to +the chain head. Use `get_block` or out-of-band head-block knowledge to +know when to stop. Notes: - Within each transaction, actions are sorted by `global_sequence` - (execution order, not schedule order). See "Receiver vs account" for why - this matters when an action queues both inlines and notifications. -- Operators on private/trusted nodes who want to scan large windows in a - single request can set `trace-max-block-range = -1` in config.ini to - remove the cap entirely. Do NOT set this on a public RPC node. + (execution order, not schedule order). See "Receiver vs account" for + why this matters when an action queues both inlines and notifications. +- The maximum supported `trace-max-block-range` is 10,000. Raise it via + `config.ini` on private/trusted nodes; public nodes should typically + leave it at the default. --- @@ -645,15 +511,14 @@ trips per backfill: ```ini # config.ini — safe on a private/trusted node -trace-max-block-range = 1000 +trace-max-block-range = 5000 ``` -`1000` is a sane upper bound for most workloads. Larger values risk -producing multi-MB responses that hit HTTP body / response-time limits -(`http-max-response-time-ms`, `http-max-body-size`) and tie up an HTTP -thread for seconds at a time on busy contracts. `-1` (unlimited) is -available for fully trusted operator-only deployments but is not -recommended even on private nodes — pick an explicit cap instead. +Values up to 10,000 are accepted. Larger windows produce larger responses +(which hit `http-max-response-time-ms` and `http-max-body-size` limits) +and tie up an HTTP thread for longer on busy contracts. Pick the largest +window that still returns in a reasonable time for your typical contract +activity. ### Detecting deposits @@ -661,7 +526,6 @@ To find all incoming transfers to your account (`exchange1111`) on `sysio.token`: ```bash -# Scan a 1000-block window (assumes trace-max-block-range = 1000) curl -s -X POST http://127.0.0.1:8888/v1/trace_api/get_token_transfers \ -H 'Content-Type: application/json' \ -d '{ @@ -670,81 +534,144 @@ curl -s -X POST http://127.0.0.1:8888/v1/trace_api/get_token_transfers \ }' | jq '.transfers[] | select(.params.to == "exchange1111")' ``` -Using `get_token_transfers` with no additional filter returns one entry per -transfer across all accounts. Filter `params.to` client-side or scan with -`get_actions` and post-filter as needed. +`get_token_transfers` with no additional filter returns one entry per +transfer across all accounts. Filter `params.to` client-side, or scan +with `get_actions` and post-filter as needed. -The `receiver="sysio.token"` preset guarantees that each on-chain transfer -appears exactly once regardless of how many accounts were notified inline. +The `receiver = account` preset guarantees that each on-chain transfer +appears exactly once regardless of how many accounts were notified +inline. ### Non-system token contracts ```bash curl -s -X POST http://127.0.0.1:8888/v1/trace_api/get_token_transfers \ -H 'Content-Type: application/json' \ - -d '{ "token_contract": "mytoken1111", "block_num_start": 1, "block_num_end": 9999999 }' + -d '{ "token_contract": "mytoken1111", "block_num_start": 1, "block_num_end": 9999 }' ``` ### Watching for smart-contract activity ```bash -# All actions executed by a DEX contract in blocks 5000–6000 +# All canonical actions executed by a DEX contract in blocks 5000–6000 curl -s -X POST http://127.0.0.1:8888/v1/trace_api/get_actions \ -H 'Content-Type: application/json' \ -d '{ "account": "my.dex", "block_num_start": 5000, "block_num_end": 6000 }' + +# Same, but including inline notifications the DEX sent to other accounts +curl -s -X POST http://127.0.0.1:8888/v1/trace_api/get_actions \ + -H 'Content-Type: application/json' \ + -d '{ "account": "my.dex", "block_num_start": 5000, "block_num_end": 6000, "include_notifications": true }' ``` ### Inline actions -Inline actions (e.g. `eosio.token::transfer` called from inside another -contract) appear as separate entries in `get_actions` results with their own -`global_sequence` values. The parent and child share the same `trx_id`. -Use `get_block` or `get_transaction_trace` if you need the full causal tree. +Inline actions (e.g. `sysio.token::transfer` called from inside another +contract) appear as separate entries in `get_actions` results with their +own `global_sequence` values. The parent and child share the same +`trx_id`. Use `get_block` or `get_transaction_trace` if you need the full +causal tree. --- -## Maintenance and retention +## ABI decoding + +When serving any trace endpoint the plugin attempts to decode the raw +`data` and `return_value` bytes of each action using the ABI captured in +`abi_log.log` at the point that action executed. + +- The ABI in effect when an action ran is used — not the current on-chain + ABI. This matters when a contract calls `setabi` mid-history: older + actions decode against the older schema. +- If the ABI is unavailable (contract never captured, ABI log missing, or + the action predates ABI capture on this node), `data` and + `return_value` are returned as raw hex. +- If ABI decoding throws (e.g., a schema/data mismatch in one contract), + the response includes a `decode_error` field and falls back to raw hex + for that action only. Unrelated actions in the same block are + unaffected. + +When decoded, `params` and `return_data` appear alongside the raw `data` +and `return_value` hex fields. + +See [ABI capture mechanics](#abi-capture-mechanics) for how the ABI log +is populated and the edge cases around same-transaction `setabi`. + +--- -### Automatic retention +## Operations -Set `trace-minimum-irreversible-history-blocks` to the number of blocks you -want to retain past LIB. Slices that fall entirely before +### Maintenance and retention + +#### Automatic retention + +Set `trace-minimum-irreversible-history-blocks` to the number of blocks +you want to retain past LIB. Slices that fall entirely before `LIB - retention_blocks` are eligible for deletion. Set `trace-minimum-uncompressed-irreversible-history-blocks` similarly to -control the compression boundary. Slices are still accessible when +control the compression boundary. Slices are still accessible when compressed but random-access reads may be slightly slower. -### Manual deletion +#### Manual deletion -Stop nodeop before deleting trace files manually. Delete the full set of +Stop nodeop before deleting trace files manually. Delete the full set of slice files for a given range (`trace_*`, `trace_index_*`, -`trace_blk_idx_*`, `trace_trx_idx_*`). Partial deletion (e.g. deleting +`trace_blk_idx_*`, `trace_trx_idx_*`). Partial deletion (e.g. removing only the trace file but not the index) will cause `bad_data_exception` errors on the next startup. -### Snapshot restores +#### Snapshot restores + +After restoring from a snapshot, the trace store's recorded range may +not match the chain's new head. If chain head is within the recorded +range, replay overlaps existing slices silently. If there's a gap, the +startup continuity check aborts (see below). + +#### abi_log.log + +`abi_log.log` is append-only. If it is lost or corrupted, delete it and +restart nodeop. The plugin will rebuild it as new `setabi` transactions +are applied or previously-unseen contracts are touched (lazy current-ABI +fetch). Historical ABI lookup for actions before the loss will fall back +to raw hex. + +--- + +### Startup continuity check -After restoring from a snapshot the trace store's recorded range may not -match the chain's new head. The plugin logs a warning at startup and -continues. If the gap is large, consider either: +On the first `block_start` signal after plugin startup, the trace +store's recorded block range is compared against the chain's current +head, and the plugin chooses one of four outcomes: -1. Copying the trace directory from a full-history node that has the missing - range, or -2. Deleting the trace directory entirely and re-syncing from genesis (only - practical for newer chains). +| Situation | Behavior | +|-----------|----------| +| No prior trace data (empty slice dir) | `info` log, fresh start; tracing begins at the current head. | +| Chain head is within `[first_recorded, last_recorded + 1]` (exact continuation OR overlap from a snapshot replay) | Silent — re-applied blocks naturally overwrite existing slice entries. | +| Chain head is **before** the first recorded block | Plugin throws; `error` log, **node shuts down**. | +| Chain head is **after** `last_recorded + 1` (forward gap) | Plugin throws; `error` log, **node shuts down**. | -### abi_log.log +The shutdown is intentional: a gap means the trace store is no longer a +faithful continuous record of chain history, and silently accepting it +would let `get_block` / `get_transaction_trace` return inconsistent data +for blocks on either side of the gap. -`abi_log.log` is append-only. If it is lost or corrupted, delete it and -restart nodeop. The plugin will rebuild it as new `setabi` transactions are -applied or previously-unseen contracts are touched (lazy current-ABI fetch); -historical ABI lookup for events before the loss -will fall back to raw hex. +To recover, pick one: + +- **Load a snapshot whose chain head is within the existing recorded + range (or one block past it).** Replay covers the existing slice + entries; tracing continues with no gap. +- **Copy the missing slice files from another node** that has the + missing range, then restart. +- **Delete the trace directory and start fresh.** Tracing resumes from + the current chain head; old block traces are lost. + +The check fires only on the first `block_start` after the plugin loads, +so recovery actions take effect on the next startup. --- -## Plugin variants +### Plugin variants Two plugin classes are registered: @@ -754,3 +681,153 @@ Two plugin classes are registered: | `trace_api_rpc_plugin` | HTTP-only: exposes endpoints against a trace directory written by another node. Use when separating the writer node from the query node. | Both accept the same configuration options. + +--- + +## Implementation details + +### On-disk layout + +All files live inside `trace-dir`. The directory is monitored by +`resource_monitor_plugin` when that plugin is loaded. + +#### Slice files + +Blocks are grouped into contiguous slices of `trace-slice-stride` blocks +each. Each slice is represented by four files that share a common range +suffix `-` (zero-padded to 10 digits): + +| File | Description | +|------|-------------| +| `trace_-.log` | Serialized `block_trace_v0` records (action data). | +| `trace_index_-.log` | Append-only metadata log of `block_entry_v0` and `lib_entry_v0` records. Source of truth; used as a fallback for `get_block` and to track LIB advancement within the slice. | +| `trace_blk_idx_-.log` | Block-offset sidecar. Enables O(1) `get_block` lookups regardless of the block's position within the slice. | +| `trace_trx_idx_-.log` | Transaction-id hash index. | + +When a slice is compressed the trace file is replaced by: + +| File | Description | +|------|-------------| +| `trace_-.clog` | zlib-compressed trace data with embedded seek points for random access. | + +The index and trx_id index files are not compressed. + +**Example** (10 000-block stride, blocks 0–29 999): + +``` +traces/ + trace_0000000000-0000010000.log + trace_index_0000000000-0000010000.log + trace_blk_idx_0000000000-0000010000.log + trace_trx_idx_0000000000-0000010000.log + trace_0000010000-0000020000.log + trace_index_0000010000-0000020000.log + trace_blk_idx_0000010000-0000020000.log + trace_trx_idx_0000010000-0000020000.log + trace_0000020000-0000030000.clog <- compressed + trace_index_0000020000-0000030000.log + trace_blk_idx_0000020000-0000030000.log + trace_trx_idx_0000020000-0000030000.log + abi_log.log +``` + +#### Block-offset index + +`trace_blk_idx_-.log` is a flat fixed-size array of 64-bit +trace-log offsets, one entry per block in the slice, used by +`/v1/trace_api/get_block` for O(1) block lookups. + +- **Header** (16 bytes): magic `BLIX`, version 1, slice width, reserved. +- **Slots** (8 bytes each): `offset + 1` into `trace_-.log`, + or 0 when the slot is empty. The `+1` encoding reserves 0 as an empty + sentinel since a block's trace data can legitimately live at offset 0. + +The sidecar is written synchronously alongside the metadata log as each +block is persisted. Forks that re-apply the same block number overwrite +the slot naturally. If the sidecar is missing or reports an empty slot, +`get_block` falls back to scanning the metadata log. + +#### Transaction-id index + +`trace_trx_idx_-.log` is a compact open-addressing hash +table (load factor ≤ 0.5, linear probing) that maps a 64-bit prefix of +a transaction SHA-256 to the block number containing that transaction. + +- **Header** (16 bytes): magic `TRIX`, version 1, bucket_count, reserved. +- **Buckets** (16 bytes each): `prefix64 (u64)` + `block_num (u32)` + + `reserved (u32)`. Empty slots have `block_num == 0`. + +On hash hit, the candidate block is scanned for a full trx_id match to +defeat 64-bit prefix collisions (a miss on the full compare re-probes +the hash table). The index is built once per slice when the slice's +last block becomes irreversible. Queries against +`/v1/trace_api/get_transaction_trace` use this index for O(1) +`trx_id → block_num` resolution. + +#### ABI log + +`abi_log.log` is an append-only file that persists the ABI published by +each contract account across all `setabi` transactions observed since +the node started (or since the file was first written). + +Format: + +``` +Header (16 bytes): magic "ABIL" (u32), version 1 (u32), reserved (u64) +Records (repeated until EOF): + account (u64) + global_seq (u64) + blob_size (u64) + blob_bytes (blob_size bytes) + crc32 (u32) over (account, global_seq, blob_size, blob_bytes) +``` + +An in-memory index keyed by `(account, global_sequence)` is built at +startup by walking the file record-by-record and validating each CRC. +Runtime lookups go through the index; the matching blob is then read +from the file via `pread()`. Appends stream new records to the end of +the file under a mutex, with no rewrite of existing records. + +Writes are not fsync'd; the on-disk tail may lose the last few records +on a kernel crash. On startup the recovery scan detects torn or +CRC-mismatched records and truncates the file at the first bad one — +any lost records are rebuilt the next time their contract is touched +(via an observed `setabi` or the lazy current-ABI fetch). + +--- + +### ABI capture mechanics + +ABI records enter `abi_log.log` through two paths: + +1. **`setabi` observation** — every time a transaction contains a + `sysio::setabi` action, the plugin decodes the action's payload and + records `(target_account, setabi.global_sequence, abi_bytes)`. This + gives exact ABI-version boundaries at the granularity of + global_sequence. + +2. **Lazy current-ABI fetch** — on the first action observed for an + account that has no prior `abi_log` entry, the plugin reads the + account's current ABI from the chain DB and records it at + `global_sequence = 0`. The `0` is a sentinel meaning "ABI as of + first observation; exact recorded sequence unknown." Lookups step + back from the query `global_sequence` to the largest recorded entry + ≤ query, so a 0 sentinel matches any action of that account that + predates the first recorded real setabi. + +#### Same-transaction `setabi` caveat + +If the plugin observes a contract for the *first* time via a transaction +that *also* contains a `setabi` for that same contract, actions on that +contract which executed *before* the setabi within that transaction +cannot be decoded and are returned as raw hex. The pre-setabi ABI is no +longer reachable from the post-apply chain state (by the time the +applied_transaction signal fires, the chain DB already reflects the new +ABI), so the plugin deliberately does not record the post-apply (new) +ABI as the contract's pre-observation baseline — doing so would decode +pre-setabi actions with the wrong schema. + +Once the contract has been observed at least once (via any earlier +transaction, or via a setabi-free transaction), later same-trx setabis +do not have this limitation: pre-setabi actions decode correctly with +the previously-recorded ABI. From c80e59ab0dfba551fe4e89bdeb8f1f73e3df5ecb Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 14 Apr 2026 14:38:07 -0500 Subject: [PATCH 21/32] trace_api: remove plan doc from repo --- docs/trace-api-history-plan.md | 307 --------------------------------- 1 file changed, 307 deletions(-) delete mode 100644 docs/trace-api-history-plan.md diff --git a/docs/trace-api-history-plan.md b/docs/trace-api-history-plan.md deleted file mode 100644 index 8b9764bb68..0000000000 --- a/docs/trace-api-history-plan.md +++ /dev/null @@ -1,307 +0,0 @@ -# trace_api_plugin: History-Solution Upgrade Plan - -Single PR, multiple commits. Each commit is independently reviewable and (where possible) independently testable. - -## Goals - -1. Fix `/v1/trace_api/get_transaction_trace` full-history scan. -2. Capture ABIs automatically (no operator-supplied `abi.json`), versioned correctly across mid-block / mid-trx setabi. -3. Refuse to start with a gap in recorded blocks (correctness foundation for #2). -4. Add exchange-friendly query endpoints over the existing trace data. - -Non-goals: account-history index, cross-contract joins, streaming, public-internet hardening, ABI sidecar pruning. - -## Storage shape (new sidecars, alongside existing slice files) - -All new files live in the same slice dir as `trace_*.log`: - -| File | Scope | Purpose | -|---|---|---| -| `trace_trx_idx_.log` | per slice | mmap'd hash table: `trx_id_prefix64 -> block_num` | -| `abi_blobs.log` | global | ABI blobs, append-only | -| `abi_index.log` | global | append-only `(account, abi_global_seq, blob_offset, blob_len)` | - -ABI sidecar is global (not per-slice) because ABI versions cross slice boundaries. - -## Commits - -### Commit 1: Continuity enforcement at startup - -**Why:** Foundation. Without this, ABI lazy-fetch reasoning breaks (you can't claim "if a setabi happened between fresh-start and first-encounter we'd have seen it" if gaps are silently allowed). - -**Changes:** -- `slice_directory`: add `last_recorded_block()` — scan highest index slice file front-to-back, return max `block_entry_v0.number`, or `nullopt` if no slice files exist. -- `store_provider`: expose `last_recorded_block()` delegating to `_slice_directory`. -- `chain_extraction_impl`: on first `signal_block_start` after startup, check: - - empty dir (`nullopt`): log fresh-start at head, proceed. - - non-empty dir: require `block_num == last_recorded_block + 1`, else invoke `except_handler` with a descriptive message ("delete slice dir or replay from snapshot covering blocks N..M"), which causes node shutdown. - -**Tests:** unit tests for all continuity cases: fresh start, exact continuation, overlap (snapshot within existing data — ok), snapshot before data start (error), gap forward (error), check fires only once. - ---- - -### Commit 2: trx_id index sidecar - -**Why:** Replace O(N slices * slice scan) with O(slices) page-faults. - -**File format:** `trace_trx_idx_.log` -``` -header { magic, version, bucket_count, bucket_size } -buckets[bucket_count] of { trx_id_prefix64, block_num, _pad } // open addressing -``` -Open-addressing hash, load factor ~0.5, linear probing. Sized at slice close from observed trx count. No bloom filter in v1 — hash probe is already O(1) per slice; revisit if not-found latency becomes a real complaint. Header has room to add a bloom section later without breaking old readers. - -**Changes:** -- `trx_id_index_writer`: builds index from a closed `trace_trx_id_.log`. Iterates entries, fills buckets, writes file. -- `trx_id_index_reader`: mmap's file, `lookup(trx_id) -> optional`. -- `slice_directory`: `find_trx_id_index_slice(slice_num)`, parallels `find_trx_id_slice`. -- Maintenance thread: when a slice's trx_id file becomes stable (LIB-crossed or compressed), build the index. Idempotent (skip if exists, rebuild if header version mismatches). -- Startup: scan slice dir, build any missing indexes synchronously before serving requests (optional fast path: do it lazily on first miss with a one-time background build). -- Rewrite `store_provider::get_trx_block_number`: - - iterate slices newest -> oldest - - per slice: bucket probe - - on hash hit, confirm by reading the actual `block_trxs_entry` to defeat 64-bit prefix collisions - - fall back to current linear scan only if a slice's index file is missing/corrupt (log warning, schedule rebuild) - -**Tests:** round-trip insert/lookup; collision handling (synthetic colliding ids); missing-index rebuild on startup; pruning when slice deleted (index file deleted alongside). - ---- - -### Commit 3: ABI sidecar — storage layer - -**Why:** Foundation for commits 4 and 5; pure data-layer with no chain hooks yet. - -**Changes:** -- `abi_blob_store`: append-only `abi_blobs.log`. `store(bytes) -> {offset, len}`. No dedup — ABIs are small (few KB) and duplication across a node's lifetime is negligible. -- `abi_version_index`: in-memory `map>`, persisted as append-only records to `abi_index.log`. Startup does a single pass over the file to populate the in-memory map. -- `lookup(account, action_global_seq) -> optional`: `upper_bound` on per-account vector, step back one. `0` sentinel matches anything. -- `record(account, abi_global_seq, abi_bytes)`: append blob, append index entry, update in-memory map. - -**Tests:** version lookup with `<=` semantics including `0` sentinel, restart replay produces identical in-memory map. - ---- - -### Commit 4: ABI capture — live + lazy - -**Why:** Wires sidecar to chain. - -**Changes:** -- In `chain_extraction_impl::on_applied_transaction`, walk action_traces: - - if `act.account == "sysio" && act.name == "setabi"`: decode payload `{account, abi: bytes}`, call `abi_version_index.record(account, act.receipt->global_sequence, abi)`. Invalidate decoder cache for that account. - - for any other action: ensure ABI exists for `act.receiver` (the contract whose code ran). If `abi_version_index.lookup(receiver, 0)` is empty, fetch current ABI from `controller.db().find(receiver)` and `record(receiver, 0, current_abi)`. (Sentinel 0 = "as of beginning of observation, exact block unknown.") -- Decoder helper `decode_action_data(account, action_global_seq, action_name, data) -> variant`: - - `lookup` ABI bytes for that version - - construct `abi_serializer` (cache by `(account, abi_global_seq)`) - - on any throw: return raw bytes + flag - -**Tests:** mid-trx setabi changes ABI for sibling inline action; lazy capture on first encounter; cache invalidation on setabi; corrupt ABI on chain falls back to raw. - ---- - -### Commit 5: `get_actions` primitive endpoint - -**Endpoint:** `POST /v1/trace_api/get_actions` - -**Request:** -```json -{ - "contract": "sysio.token", // required - "action": "transfer", // optional - "block_num": 12345, // OR - "block_range": [12345, 12400], // capped by trace-max-block-range - "filters": { - "from": "alice", // optional - "to": "bob", // optional - "authorizer": "alice" // optional - }, - "include_failed": false // default false; when false, only "executed" -} -``` - -**Behavior:** -- Always traverses inline actions (no toggle). -- Match rule: `act.account == contract` (canonical action, not notification). Notifications surface as separate rows when `act.receiver == contract` but `act.account != contract` — excluded by default; reconsider only if needed. -- Range scan: iterate `block_num` in range, load block trace from existing slice infra, walk action_traces. -- Filters applied post-load. -- ABI decode via commit-4 helper; raw fallback on error. - -**Response row:** -```json -{ - "block_num": 12345, - "trx_id": "...", - "action_ordinal": 3, - "global_sequence": 9876543, - "receiver": "sysio.token", - "account": "sysio.token", - "name": "transfer", - "authorization": [...], - "status": "executed", - "irreversible": true, - "data_decoded": {...}, // present if decode succeeded - "data_raw": "..." // present only if decode failed -} -``` - -**Config:** -- `trace-max-block-range` (default e.g. 1000; -1 = unlimited for local-only deployments). - -**Tests:** filters; failed-trx exclusion/inclusion; inline traversal; range cap; decode-failure raw fallback. - ---- - -### Commit 6: `get_token_transfers` convenience endpoint - -**Endpoint:** `POST /v1/trace_api/get_token_transfers` - -**Request:** same shape as `get_actions` minus `action`, plus: -```json -{ - "contract": "sysio.token", // required, exact - "account": "alice", // optional, matches from OR to - "memo_contains": "user_42", // optional substring - "block_num" | "block_range": ..., - "include_failed": false -} -``` - -**Behavior:** thin wrapper over `get_actions` with `action="transfer"`, projects decoded data: -```json -{ - "block_num": ..., - "trx_id": ..., - "action_ordinal": ..., - "global_sequence": ..., - "from": "...", - "to": "...", - "quantity": "1.0000 SYS", - "memo": "...", - "status": "executed", - "irreversible": true -} -``` - -If decode fails (ABI invalid), the row is **omitted** (not raw — exchanges can't use raw transfers; they'd see them via `get_actions` if needed). Log a warning with trx_id for diagnosis. - -**Tests:** from/to OR semantics; memo substring; multiple transfers in one block; decode-failure omission. - ---- - -## Risks / open items - -- **Reorg of indexed data.** Trx_id index covers irreversible blocks only. With fast finality the reversible window is tiny (seconds, low single-digit blocks), and most operators are expected to run trace_api in read-mode=irreversible. Lookup path: index probe first; on miss, linear-scan the reversible window (cheap because the window is small) before returning not-found. No reorg fixup needed. -- **`abi_index.log` corruption.** Append-only with no checksums today. Acceptable v1; add CRC per record in a follow-up if it bites. -- **Continuity check + existing deployments.** Operators with existing slice dirs and gaps will fail to start after upgrade. Release note + `trace-allow-gap=true` escape hatch. - -## Test plan - -The trace_api_plugin has unusually strong test coverage today (see `plugins/trace_api_plugin/test/`: `test_trace_file`, `test_data_handlers`, `test_extraction`, `test_responses`, `test_compressed_file`, `test_configuration_utils`). Maintain that bar — every new component gets a dedicated test file with the same breadth. - -### Commit 1 — continuity enforcement - -New test file: `test/test_continuity.cpp`. - -- Empty slice dir, chain head at block 1 -> starts, logs fresh-start. -- Empty slice dir, chain head at block 50M -> starts, logs fresh-start at 50M. -- Non-empty slice dir ending at block N, chain head at N+1 -> starts normally. -- Non-empty slice dir ending at block N, chain head at N+10 -> throws with operator-facing message naming the gap. -- Non-empty slice dir ending at block N, chain head at N-5 (replay from snapshot behind tip) -> starts normally once chain catches up; the first accepted_block after startup is N+1. -- `trace-allow-gap=true` config override -> legacy behavior, warns loudly. -- Slice dir contains only orphaned index file (no trace/trx_id) -> treat as empty (or throw — pick and test). -- Corrupted highest slice file -> clear error, does not proceed. - -### Commit 2 — trx_id index - -New test file: `test/test_trx_id_index.cpp`. - -- Writer: build from synthetic `trace_trx_id_.log`, verify bucket occupancy, linear probe chains terminate, load factor within spec. -- Reader: lookup hit, lookup miss, lookup with prefix collision (two trx_ids sharing the 64-bit prefix) -> second probe, correctness verified against underlying trx_id entry. -- Round-trip: random 10k trx_ids, every one found; 10k never-inserted trx_ids, all return not-found. -- File format: wrong magic -> error; wrong version -> error; truncated file -> error (not UB). -- mmap lifecycle: reader holds file, writer cannot overwrite (or does via new file + rename). -- Startup missing-index rebuild: delete one slice's index, start, verify it's rebuilt, lookup still works. -- Corrupt index file on startup: rebuilt from the trx_id log source-of-truth. - -Extensions to existing tests: - -- `test_extraction.cpp`: verify index is built when slice closes (on LIB cross). -- Integration test under `tests/`: run against a 100k-block replay dataset, measure and assert `get_transaction_trace` latency under a fixed budget (regression guard). -- Reversible window: lookup for a trx in the current reversible window hits via the fallback linear scan. - -### Commit 3 — ABI sidecar storage - -New test file: `test/test_abi_sidecar.cpp`. - -- `abi_blob_store` append + read round-trip across many records. -- `abi_version_index`: - - `lookup(acct, global_seq)` with `<=` semantics: boundary at exact match, just-before, just-after. - - `0` sentinel entry matches any `global_seq`. - - Multiple versions same account, different global_seqs -> returns correct one. - - Lookup for unknown account -> empty. - - Lookup with `global_seq` before the earliest recorded -> empty (not the `0` entry, unless a `0` entry exists). -- Restart replay: write N records across process lifetime, restart, in-memory map identical to pre-restart (compare via dump-to-vector). -- Corrupt `abi_index.log` tail: recover up to last valid record, log; do not crash. -- Mid-block multiple setabi for same account: both stored, lookup between them returns the earlier. - -### Commit 4 — ABI capture - -Extensions to `test_extraction.cpp` (add new cases; do not fork the file unless it grows unwieldy): - -- Live capture: `setabi` action in applied_transaction -> sidecar has new record with correct `global_sequence`. -- Lazy capture: first action from never-before-seen account -> `lookup(account, 0)` populated from controller state. -- Cache invalidation: action -> lazy fetch; then setabi observed; next decode for that account uses the new ABI. -- Mid-trx setabi: trx has action A (account X), then setabi for X, then action B (account X). A decodes with old ABI, B decodes with new ABI. Verified via action_global_seq boundaries. -- Inline setabi: same as above but setabi is inline, not top-level. -- Plugin started at block 50M on a chain where X did setabi at block 40M and never again: first encounter lazy-fetches current ABI tagged at 0 — decode succeeds. -- Invalid ABI on chain (bytes that don't deserialize into an `abi_def`): sidecar records raw bytes; decoder returns raw on decode attempt; does not throw through to endpoint. -- Decoder cache hit counting (optional, via hook): verify cache actually reused across same `(account, abi_global_seq)`. - -### Commit 5 — get_actions endpoint - -Extensions to `test_responses.cpp`: - -- Single block, single contract, single matching action -> returns one row with decoded data. -- Single block, no matching actions -> empty response, 200. -- Multiple matching actions across inline tree -> all returned in action_ordinal order. -- Filters combined: `contract + action`, `+from`, `+to`, `+authorizer`, mutually constraining. -- `include_failed=false` (default): `soft_fail`, `hard_fail`, `expired`, `delayed` excluded. -- `include_failed=true`: all statuses returned with correct `status` field. -- Block range: full range scanned, results in order. -- Range cap: request exceeding `trace-max-block-range` -> 400 with the cap value. -- `max_block_range=-1` (unlimited): large range works. -- ABI present, decode succeeds: `data_decoded` present, no `data_raw`. -- ABI present, decode fails (invalid bytes for the schema): `data_raw` present, `data_decoded` absent, with `decode_error`. -- Reversible block in range: included with `irreversible: false`. -- Contract the endpoint has never seen an ABI for: lazy fetch kicks in at query time or ingestion time (confirm which; test accordingly). -- Notifications vs canonical actions: only `act.account == contract` rows returned (notifications excluded). - -### Commit 6 — get_token_transfers - -New test file: `test/test_token_transfers.cpp`. - -- Simple transfer: `from/to/quantity/memo` projected correctly. -- `account` filter = from OR to: matches either side. -- `memo_contains`: substring match, case-sensitive; non-match excluded; empty memo never matches a non-empty pattern. -- Multiple transfers in one block: all returned, ordered by `(block_num, action_ordinal)`. -- Inline-action transfer: returned. -- Failed transfer (hard_fail): excluded by default, included with `include_failed=true`. -- ABI decode failure: row omitted, warning logged, endpoint succeeds. -- Non-token contract with a `transfer` action of different schema: decode fails -> row omitted. (Caller was told to scope correctly; we just don't return garbage.) -- `contract` required -> 400 when omitted. -- Range semantics match `get_actions` (reuse shared test helpers). - -### Cross-cutting integration tests (under `tests/`) - -- End-to-end: launch a TestHarness cluster with trace_api enabled, deploy sysio.token, perform 1000 transfers with a mix of inline/top-level, setabi mid-run on the token contract changing the memo type, then query: - - `get_transaction_trace` for each trx_id — all hit, latency asserted. - - `get_token_transfers` for the token, each block in range, all transfers accounted for, old transfers decoded with old ABI, new ones with new ABI. -- Gap-refusal: kill the node, advance chain via a second node, restart first node with trace_api -> refuse to start. -- Restart idempotency: start, run 1000 blocks, stop, restart — sidecars/indexes intact, queries unchanged. -- Compressed slice interaction: force a slice compression, verify trx_id lookup still works against the `.clog` slice (via the existing compressed-slice read path). -- Retention pruning: set `minimum_irreversible_history_blocks` low, verify slice deletion also removes the matching `trace_trx_idx_*` file; verify ABI sidecar is untouched. - -### Performance regression guard - -Add a micro-benchmark (gated, not run in default ctest): - -- 100k-block synthetic trace corpus, 10k random trx_id lookups: p50 and p99 latency must be within a configured budget (e.g., p99 < 5 ms on SSD). Fails the guard if we regress to linear-scan behavior. From af5be9d4262e7674273cdcc50fc1ba4f40fa5930 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 14 Apr 2026 15:49:16 -0500 Subject: [PATCH 22/32] trace_api: nit follow-ups + regenerate snapshot reference data - chain_extraction: fmt::format for continuity error messages; drop _ prefix on abi_fetcher / startup_checked members for consistency with the rest of the file - abi_data_handler: clarify serialize_to_variant comment -- it's the active path for get_block / get_transaction_trace, not legacy - trace, request_handler, store_provider: whitespace / comment nits - tests/sysio_util_snapshot_info_test: flush NamedTemporaryFile before sys-util reads it (v2 format reads the footer, so the file must be fully on disk); update head_block_id after regen - Regenerate unittests/snapshots/* and consensus_blockchain/snapshot with the corrected WIRE magic number --- .../sysio/trace_api/abi_data_handler.hpp | 8 ++-- .../sysio/trace_api/chain_extraction.hpp | 43 ++++++++---------- .../include/sysio/trace_api/trace.hpp | 34 +++++++------- .../trace_api_plugin/src/request_handler.cpp | 7 ++- .../trace_api_plugin/src/store_provider.cpp | 13 ++++-- tests/sysio_util_snapshot_info_test.py | 3 +- unittests/snapshots/blocks.log | Bin 138266 -> 137428 bytes unittests/snapshots/snap_v1.bin.gz | Bin 51851 -> 51295 bytes unittests/snapshots/snap_v1.bin.json.gz | Bin 90285 -> 89317 bytes unittests/snapshots/snap_v1.json.gz | Bin 89988 -> 89001 bytes .../test-data/consensus_blockchain/snapshot | Bin 2149329 -> 2149329 bytes 11 files changed, 56 insertions(+), 52 deletions(-) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp index 905c8e6d72..29ec02cf6b 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/abi_data_handler.hpp @@ -81,9 +81,11 @@ namespace sysio { decode_result decode(const action_trace_v0& action); /** - * Legacy convenience wrapper: returns only {params, return_data} and drops - * the status/error fields. Retained for callers that still expect the - * tuple shape; prefer decode() for new code. + * Tuple-shape wrapper used by the response_formatter::process_block pipeline + * (get_block / get_transaction_trace), whose data_handler_function is keyed + * to the {params, return_data} tuple. Returns empty variants on decode + * failure -- callers that need the decode error surfaced (get_actions / + * get_token_transfers) use decode() directly. */ std::tuple> serialize_to_variant(const std::variant& action); diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp index 3e8339bd72..a51714a601 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/chain_extraction.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -42,7 +43,7 @@ class chain_extraction_impl_type { abi_fetcher_t abi_fetcher = {} ) : store(std::move(store)) , except_handler(std::move(except_handler)) - , _abi_fetcher(std::move(abi_fetcher)) + , abi_fetcher(std::move(abi_fetcher)) {} /// connect to chain controller applied_transaction signal @@ -118,12 +119,12 @@ class chain_extraction_impl_type { // (lazy or setabi), we never re-fetch. Using the log as // source-of-truth avoids holding a per-node-lifetime set of all // accounts ever observed. - if (_abi_fetcher + if (abi_fetcher && setabi_targets_this_trx.count(account) == 0 && !store.has_abi_entry(account)) { try { - if (auto abi = _abi_fetcher(account)) + if (auto abi = abi_fetcher(account)) store.append_abi(account, 0, std::move(*abi)); } catch (const std::exception& e) { fc_dlog(_log, "trace_api: lazy ABI fetch for {} failed: {}", account, e.what()); @@ -159,8 +160,8 @@ class chain_extraction_impl_type { } void on_block_start( uint32_t block_num ) { - if (!_startup_checked) { - _startup_checked = true; + if (!startup_checked) { + startup_checked = true; check_continuity(block_num); } clear_caches(); @@ -181,24 +182,20 @@ class chain_extraction_impl_type { return; if (block_num < first) { - throw std::runtime_error( - std::string("trace_api: chain head (") + std::to_string(block_num) + - ") is before the first recorded trace block (" + std::to_string(first) + - "). To recover: load a snapshot whose chain head is within [" + - std::to_string(first) + ", " + std::to_string(last + 1) + - "], or copy the trace files covering blocks " + std::to_string(block_num) + - ".." + std::to_string(first - 1) + " from another node, or delete the " - "trace directory to start fresh (loses historical traces)."); + throw std::runtime_error(fmt::format( + "trace_api: chain head ({}) is before the first recorded trace block ({}). " + "To recover: load a snapshot whose chain head is within [{}, {}], " + "or copy the trace files covering blocks {}..{} from another node, " + "or delete the trace directory to start fresh (loses historical traces).", + block_num, first, first, last + 1, block_num, first - 1)); } // block_num > last + 1: forward gap - throw std::runtime_error( - std::string("trace_api: gap detected in trace data. Last recorded block: ") + - std::to_string(last) + ", current block: " + std::to_string(block_num) + - ". To recover: load a snapshot covering block " + std::to_string(last + 1) + - " (or earlier within the recorded range), or copy the trace files covering " - "blocks " + std::to_string(last + 1) + ".." + std::to_string(block_num - 1) + - " from another node, or delete the trace directory to start fresh " - "(loses historical traces)."); + throw std::runtime_error(fmt::format( + "trace_api: gap detected in trace data. Last recorded block: {}, current block: {}. " + "To recover: load a snapshot covering block {} (or earlier within the recorded range), " + "or copy the trace files covering blocks {}..{} from another node, " + "or delete the trace directory to start fresh (loses historical traces).", + last, block_num, last + 1, last + 1, block_num - 1)); } catch (const yield_exception&) { // Order matters: yield_exception propagates (it's the signal that the // plugin's own except_handler uses to unwind the controller), while @@ -258,10 +255,10 @@ class chain_extraction_impl_type { private: StoreProvider store; exception_handler except_handler; - abi_fetcher_t _abi_fetcher; + abi_fetcher_t abi_fetcher; std::map cached_traces; std::optional onblock_trace; - bool _startup_checked{false}; + bool startup_checked{false}; }; diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/trace.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/trace.hpp index 3c12836caa..2c0e68b576 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/trace.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/trace.hpp @@ -19,23 +19,23 @@ namespace sysio::trace_api { }; struct action_trace_v0 { - fc::unsigned_int action_ordinal = {}; - fc::unsigned_int creator_action_ordinal = {}; - fc::unsigned_int closest_unnotified_ancestor_action_ordinal = {}; - uint64_t global_sequence = {}; - uint64_t recv_sequence = {}; - boost::container::flat_map auth_sequence = {}; - fc::unsigned_int code_sequence = {}; - fc::unsigned_int abi_sequence = {}; - chain::name receiver = {}; - chain::name account = {}; - chain::name action = {}; - std::vector authorization = {}; - chain::bytes data = {}; - chain::bytes return_value = {}; - std::vector account_ram_deltas = {}; - std::optional cpu_usage_us = {}; - std::optional net_usage = {}; + fc::unsigned_int action_ordinal = {}; + fc::unsigned_int creator_action_ordinal = {}; + fc::unsigned_int closest_unnotified_ancestor_action_ordinal = {}; + uint64_t global_sequence = {}; + uint64_t recv_sequence = {}; + boost::container::flat_map auth_sequence = {}; + fc::unsigned_int code_sequence = {}; + fc::unsigned_int abi_sequence = {}; + chain::name receiver = {}; + chain::name account = {}; + chain::name action = {}; + std::vector authorization = {}; + chain::bytes data = {}; + chain::bytes return_value = {}; + std::vector account_ram_deltas = {}; + std::optional cpu_usage_us = {}; + std::optional net_usage = {}; }; struct transaction_trace_v0 { diff --git a/plugins/trace_api_plugin/src/request_handler.cpp b/plugins/trace_api_plugin/src/request_handler.cpp index d8c2b3a38d..f28afa3595 100644 --- a/plugins/trace_api_plugin/src/request_handler.cpp +++ b/plugins/trace_api_plugin/src/request_handler.cpp @@ -7,8 +7,8 @@ namespace sysio::trace_api { fc::mutable_variant_object build_action_variant(const action_trace_v0& a, - const decoded_action& decoded, - variant_shape shape) { + const decoded_action& decoded, + variant_shape shape) { fc::mutable_variant_object v; // Fields common to all shapes. v("global_sequence", a.global_sequence) @@ -65,8 +65,7 @@ namespace { fc::variants process_actions(const std::vector& actions, const data_handler_function& data_handler) { fc::variants result; result.reserve(actions.size()); - // global_sequence is unique per action (chain invariant), so sort stability - // is not required. + // global_sequence is unique per action (chain invariant), so sort stability is not required. std::vector sorted; sorted.reserve(actions.size()); for (const auto& a : actions) sorted.push_back(&a); diff --git a/plugins/trace_api_plugin/src/store_provider.cpp b/plugins/trace_api_plugin/src/store_provider.cpp index 68d3b84215..7b1e010c0d 100644 --- a/plugins/trace_api_plugin/src/store_provider.cpp +++ b/plugins/trace_api_plugin/src/store_provider.cpp @@ -200,18 +200,21 @@ namespace sysio::trace_api { break; } } + // block can be seen again when a fork happens, if not in the new block remove it from blocks that have the trx if (!found_in_block) trx_block_nums.erase(trxs_entry.block_num); } else if (std::holds_alternative(entry)) { auto lib = std::get(entry).lib; if (!trx_block_nums.empty() && lib >= *(--trx_block_nums.end())) { - return false; + return false; // *(--trx_block_nums.end()) is the block with highest block number which is final } } else { - FC_ASSERT(false, "unpacked data should be a block_trxs_entry or a lib_entry_v0"); + FC_ASSERT( false, "unpacked data should be a block_trxs_entry or a lib_entry_v0" ); } offset = trx_id_file.tellp(); } + // if empty() keep searching + // if not empty() then we have found the trx and since traversing in reverse order this should be the latest return trx_block_nums.empty(); }); @@ -400,10 +403,11 @@ namespace sysio::trace_api { if (name.rfind(_trace_index_prefix, 0) == 0 && entry.path().extension() == _trace_ext) paths.push_back(entry.path()); } - if (ascending) + if (ascending) { std::sort(paths.begin(), paths.end()); - else + } else { std::sort(paths.begin(), paths.end(), std::greater<>()); + } return paths; } @@ -431,6 +435,7 @@ namespace sysio::trace_api { return std::make_pair(*lo, *hi); } catch (...) { // malformed or partially written slice + fc_wlog(_log, "trace_api: cannot scan index slice '{}'", path.string()); } return std::nullopt; } diff --git a/tests/sysio_util_snapshot_info_test.py b/tests/sysio_util_snapshot_info_test.py index d2e4a1fdbd..ae589f74a4 100755 --- a/tests/sysio_util_snapshot_info_test.py +++ b/tests/sysio_util_snapshot_info_test.py @@ -16,7 +16,7 @@ "result": { "version": 1, "chain_id": "144035215e20fd016e2b4b065349c959a1070fcbb0dc3f4784f3130685e774fc", - "head_block_id": "0000001d6e07189b77f366197e2fe3b797323bc51967d3eff34379c8ea24cd2b", + "head_block_id": "0000001d7105e950ff4a577b653dd4c75a805be942fd49f1da6f8e4642516925", "head_block_num": 29, "head_block_time": "2025-01-01T00:00:14.000" } @@ -28,6 +28,7 @@ def test_success(): with gzip.open(test['file'], 'rb') as compressed_snap_file: with tempfile.NamedTemporaryFile('wb') as uncompressed_snap_file: shutil.copyfileobj(compressed_snap_file, uncompressed_snap_file) + uncompressed_snap_file.flush() assert(test['result'] == json.loads(Utils.processSysioUtilCmd(f"snapshot info {uncompressed_snap_file.name}", "do snap info", silentErrors=False, exitOnError=True))) def test_failure(): diff --git a/unittests/snapshots/blocks.log b/unittests/snapshots/blocks.log index adde7a3d2e76639360ecd426d03228b3ca375e23..eb27644e88b98fd7f806422279a300702e3a58f0 100644 GIT binary patch delta 2856 zcmbVO4Qy0Z7QW}a_h#NOGt4ctopws)zQ;iNhbd}?7H#RJN>QM-AR-7V7^A*!vuw$d zB`X~?CK#YVtZ-3Laf8XK5p{#k#u%10yNWReRFrB&R7`48G>~8x5z%wso3<0TCdQfM z-TUsl_nhyX^PO|dafPq13#+DrvH`>T)oUl&jEy^31H&QyVjll^wa-M-G8w4?vlQ` zZLhCyZEQXVkL3RKo;ujjkzO?az2migPqWgpbKlk8{KN67FL{ZJuUPSl<%g4BcZ zVQh0&eNl!f_2q|9I{tVsW9-q)ig*9R*gL&B`y=Q;>}XfZ2B4;-9HR7_q#x1p;{0Dd zJOHb#)#G;kxjlD&0H#DH??l7OVlQN;Fe$oROf!LwA<;1f$U{uk4Z)qo{zAxT5tg*Q zYJ)jpVX4_eFf(gO?m3n)^k9gP%zADp&Y}VZlLL%5yOAn1GicKS&6pd>psD8>Bs~K7 z+>}5myflkG0|sPa1c*UE41u+V-Dub(zt|H%gYEJ80yP9vh8;_NyA`IXS5LtNHLVYd zB3v-qhJ1mNeNY-oMd^r8mrlW>a9wWqSHNNRKJ@j3^9GxwhY;oUn1LQM&|?b2U#A_k zK!Qy^UoF@So7Js@keF@@TSUY-5#>eEK(8?PP1qvLDzx3ub#W~@l);z?@sv6uAgKYF zQsfQ{!cqfYK@}f@2gb=5`Of#ax_k&8iMOSMMP8}%B=B3*ks){jmgl-pg9WgxfB)G@ zx$WP;3kb_p(;2uLZp*Dd1B=7S69|^JX)olXC}5_s1Jp~~V7Y2K58;;c%+oNrBbj4K z5RBYNdT3deNhTRjxTa+D2sX;owAbX#KxYn`zVs1I13iwFH9*20IvNr)==_ zXFV(Pl(QZtPCd#-mIm@uY$5+^WNl;y87n_mtJZ^~{YS@?bO5-sLp)})fG%~$_tCK!6AUvhV4@swc+Y$6I7N2pJc*8Vfdei zKP4{-7-oVe&{e*^-kq*%hDqcXep7pG$w)uqOg z8=;3j=`5r(rhl+Y$En$6+Rs+t=Ma?)Etk0KiO@BKBs9l<$O#c3QbG^kyZVFxyYlMnShXQ${-T6+s8d^EMywzXs6-zmN9xy&zUNCvMXy05 z#}oI-x$3nIa98X9Mc=QNlqHnazCIoQdYv%C#gljbDkW4BFp;7aQ+6v zc*+7i8L-va4KSStDts>K8l6-ZH$Y`3p*fQns(X?OebzNAf_HTjognTktL?GP;P07{4Iw(_@3y3GH z+^{A~+O@mfh@RQ{VX9r5p+$F!XqGw$36oxMy@5l9_vBvt-8$?~y} z-N^E6D{N#PCDezgk`N6kKQy&Y)}h(VAcfQn9`*}m)@AZMXZeGuHsk( q#kZ#-S@!5 zgFpM`&0qiH2d4k|&6N+nw0r+UM_xVr8T%{cvCF@G?di{+UeoKJeRb>bS68hXKeM-^ zcW?6mz4Q8Y<7NL}ggCsJ(p{~xJ$>Z`8W-%^OhSlbavdwU|6XmYv8AluQ+o8Vcn!p(r{a;)gLb-k37FFn*bKjp8XIr_f7%p08(#w=&lRbVh^Tg#|f@ku}ygPYwk2m*fo4a~*(YI0m?fk#=P~jjO4uUf?2ib^18~cRXF>AxR(CJGyD<{(kY9*WSpBS7t+ro=A2Nu|YF8tu> zmvmydoB6v>a zpUowRI_Rc#`sDHTv@@*kf(R{IOIhx1e6;>ZuCHb z@I0!lHm~gAr{0)Q{rL&%wd8LpJ^wfQR(kRz&1zcIj+Bv6R~%9B(X{gvJxU*|K6{EB zqQUC#UZX>j2GgCV={j0fJ#v}`-SID4q;qFC8ALz<&bGEt`s6lRoo?`@OJ~yEZ_&7p zGa`=KA|!Co655ei0+nQRT%f3e`4u}NC6Hd2I+GrIi`tTCPTVIrsU$M6cK58P-#$Nb zd$fMb>$;WXAuvO)8I&0S8D=XRfj|mMg&RQ0PQtCzBfp!C+o=z7w^NLQTF@T7EP@Fk z`vo_|XouS^6!*yOyk10;u#P99zaC(H^?T9d>4LXuc-BE8$+dMrX#XH22wN~0$mzvF zgdvE7J`5_>lihL9KfHERYkKZ1eIvd1EOifmXU)X)`~RT#r?0(3t!eW)>PlyvH+MTG zFESe~HXAK58}TGIx}kdD9PK0jn3Rm$T;S=4{zX#~6*I`9s3QR&v!b!U69j?bn^8sC z{2YtysAXM@bo_|m+D0CtFd2~~XSl{6yGw|$5G6~S5Lk_q*#==j74T-1th?alvh))l zHC~7|(wFeeH$~`Zz7W;Rpf12O9Zv)S1-a z(r{5_WdGa<1?4v%MYC4TUmC$Zjpxc6 zNtK)6F7xO@Y~|ENsyt&^PP(j7H5LbATjrH08B(I5!(zB(|ud1y^&U2 z!O)7s>|U`}yfV~Up~D{0D%&9RbjNYAR!OFnSJTQww`iiv-4feooRZ64gw3?vRj!#rTKQ1>H2Ki<4`e98=!)dF+L9Mt$$G8ureb46+1bGAK+>P@=jO=Ad2F z0~-J`wT-ieg7#UoZ%Cvq=?4 zA5x0!FI)`xnx!q3`qBcsHao;-OAT6Cb%BVg5r|_7iZ?9fxR5ITzpd2$-35DUqeU9?MTpVFO<$;@-mZ1XEa#8u58UAqlfVzVlXob+0;gL#{ z!oc4@?3y%&DYq^{GyheII5F_v1a4?jg%R>K+-rmw?yZZ%mzNRa{ukS_toJ~T!|xkt zy@P|qW-?C0Or|ycohEbY|G~_;+Dz+0GdV#N%dWK;ITGSP;py-%-)u{Cm1e=?H%7$| zkuxESMo^6d2BP4Ag$~YVY!i+q{I84V9w~|>@Nh`S;ii@m$Y$WU&*vgdIoK6!gG}-G zh~B!CKVLvubS5|(;sa7J#(2u*t#8H#(ttb oqKfpD^sm8LRPp(9b^6E#nv%|Jmh0%YYIi~}+0}dBBUd>80Y%(`5C8xG diff --git a/unittests/snapshots/snap_v1.bin.gz b/unittests/snapshots/snap_v1.bin.gz index 1f3100911a7e2694efc52988ea5e4d64fb55c036..32c18f6e11237bf46cd4f99276c5c736125a166a 100644 GIT binary patch literal 51295 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 literal 51851 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

m$9(hhBF^$q;$ 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 z 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 z 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 delta 62948 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 delta 113 zcmWN@yA6T>06@W@&mTV#6%iC%hQii{c?@|oIEhmj3O17BlAAt5FwN**&;E@BvXG_B rU&&fFvXz~L5=ktHq_USx4wB1JPI8t)E>g->DyiircWI>k@)&=A6&o6n From 51139e5b24505600c3c13f19be2fd29d6cc4f7eb Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 14 Apr 2026 17:07:43 -0500 Subject: [PATCH 23/32] trace_api: update tests for action -> name field rename Three tests read transaction_trace responses from the trace_api and accessed the action name via transaction["actions"][0]["action"]. That field was renamed to "name" in this PR; update the call sites. --- tests/PerformanceHarness/performance_test_basic.py | 2 +- tests/nodeop_lib_test.py | 4 ++-- tests/nodeop_run_test.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/PerformanceHarness/performance_test_basic.py b/tests/PerformanceHarness/performance_test_basic.py index f56a1833f5..3d647da69b 100755 --- a/tests/PerformanceHarness/performance_test_basic.py +++ b/tests/PerformanceHarness/performance_test_basic.py @@ -300,7 +300,7 @@ def fileOpenMode(self, filePath) -> str: return append_write def isOnBlockTransaction(self, transaction): - if transaction['actions'][0]['account'] != 'sysio' or transaction['actions'][0]['action'] != 'onblock': + if transaction['actions'][0]['account'] != 'sysio' or transaction['actions'][0]['name'] != 'onblock': return False return True diff --git a/tests/nodeop_lib_test.py b/tests/nodeop_lib_test.py index 2fe3927419..2d93ed25a9 100755 --- a/tests/nodeop_lib_test.py +++ b/tests/nodeop_lib_test.py @@ -289,8 +289,8 @@ amountVal=None key="" try: - key = "[actions][0][action]" - typeVal = transaction["actions"][0]["action"] + key = "[actions][0][name]" + typeVal = transaction["actions"][0]["name"] key = "[actions][0][params][quantity]" amountVal = transaction["actions"][0]["params"]["quantity"] amountVal = int(decimal.Decimal(amountVal.split()[0]) * 10000) diff --git a/tests/nodeop_run_test.py b/tests/nodeop_run_test.py index c4cbf5eb66..ba38691488 100755 --- a/tests/nodeop_run_test.py +++ b/tests/nodeop_run_test.py @@ -287,8 +287,8 @@ amountVal=None key="" try: - key = "[actions][0][action]" - typeVal = transaction["actions"][0]["action"] + key = "[actions][0][name]" + typeVal = transaction["actions"][0]["name"] key = "[actions][0][params][quantity]" amountVal = transaction["actions"][0]["params"]["quantity"] amountVal = int(decimal.Decimal(amountVal.split()[0]) * 10000) From 0b4267120c17104ec2e01c3b79a96dba87d0db22 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 22 Apr 2026 14:38:02 -0500 Subject: [PATCH 24/32] trace_api: get_actions_impl - filter before sort, hoist invariants out of hot loop Filter actions before the global_sequence sort so transactions whose actions are all rejected by the receiver/account/action filter skip the sort entirely - the common case when a request scans thousands of blocks. std::sort replaced with std::ranges::sort and a member-pointer projection on action_trace_v0::global_sequence. Also in get_actions_impl: - Reuse the matches vector across trxs/blocks; clear() keeps capacity so repeat scans avoid per-trx allocations. - Hoist trx.id.str() and the other trx-level fields (block_num, block_time, producer_block_id) out of the match emit loop; a multi-match trx no longer repeats the checksum->hex conversion. - Materialize has_receiver/has_account/has_action + unwrapped chain::name values once per call; inner predicate compares names directly instead of dereferencing std::optional each time. All 11 get_actions_tests cases and the full 97-case trace_api suite pass. --- .../sysio/trace_api/request_handler.hpp | 75 ++++++++++++------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp index c8ceaddbd4..3cd46df931 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -185,6 +186,19 @@ namespace sysio::trace_api { actions_result get_actions_impl(const action_query& query, variant_shape shape) { actions_result result; + // Hoist filter state out of the hot loop: avoids re-loading the optional's discriminator and value on every + // action comparison in the inner scan. + const bool has_receiver = query.receiver.has_value(); + const bool has_account = query.account.has_value(); + const bool has_action = query.action.has_value(); + const chain::name receiver_v = has_receiver ? *query.receiver : chain::name{}; + const chain::name account_v = has_account ? *query.account : chain::name{}; + const chain::name action_v = has_action ? *query.action : chain::name{}; + + // Reused across all transactions in all blocks: clear() keeps the vector's capacity so repeated scans of + // trxs with similar action counts avoid per-trx allocations. + std::vector matches; + const uint32_t end = query.block_num_end; for (uint32_t block_num = query.block_num_start; block_num <= end; ++block_num) { auto data = logfile_provider.get_block(block_num); @@ -192,39 +206,42 @@ namespace sysio::trace_api { std::visit([&](const auto& bt) { for (const auto& trx : bt.transactions) { - // trx.actions is stored in schedule order (how the chain's apply_context - // scheduled action slots), which is NOT global_sequence order when an - // action queues both inline actions and require_recipient notifications: - // notifications run before inlines, so the inline's global_sequence is - // higher than later-scheduled notifications'. Sort pointers by - // global_sequence so clients always see execution order (matches - // chain_plugin's push_transaction and the legacy get_block response). - // global_sequence is unique per action (chain invariant), so sort - // stability is not required. - std::vector sorted; - sorted.reserve(trx.actions.size()); - for (const auto& a : trx.actions) - sorted.push_back(&a); - std::sort(sorted.begin(), sorted.end(), [](const auto* l, const auto* r){ - return l->global_sequence < r->global_sequence; - }); - - for (const action_trace_v0* ap : sorted) { - const auto& a = *ap; - if (query.receiver && a.receiver != *query.receiver) continue; - if (query.account && a.account != *query.account) continue; - if (query.action && a.action != *query.action) continue; + // Filter first, sort after. trx.actions is stored in schedule order (how apply_context scheduled + // action slots), which is NOT global_sequence order when a parent action queues both inline actions + // and require_recipient notifications: notifications run before inlines, so the inline's + // global_sequence is higher than later-scheduled notifications'. Sort the matches by + // global_sequence so clients see execution order, matching chain_plugin's push_transaction response. + // global_sequence is unique per action, so sort stability is not required. Sorting only after + // filtering avoids the cost for transactions whose actions are all rejected by the filter - the + // common case when scanning for a specific receiver/account/action across a wide block range. + matches.clear(); + for (const auto& a : trx.actions) { + if (has_receiver && a.receiver != receiver_v) continue; + if (has_account && a.account != account_v) continue; + if (has_action && a.action != action_v) continue; + matches.push_back(&a); + } + if (matches.empty()) continue; + std::ranges::sort(matches, {}, &action_trace_v0::global_sequence); + + // Hoist per-trx variant fields so a multi-match trx doesn't repeat the checksum->hex conversion or + // re-read the same block-level members for each emitted action. + const std::string trx_id_str = trx.id.str(); + const uint32_t trx_block = trx.block_num; + const auto& trx_time = trx.block_time; + const auto& trx_pbid = trx.producer_block_id; - // Decode via the provider; build the variant via the shared - // helper so get_actions / get_token_transfers / get_block all - // agree on field shapes. + for (const action_trace_v0* ap : matches) { + const auto& a = *ap; + // Decode via the provider; build the variant via the shared helper so get_actions / + // get_token_transfers / get_block all agree on field shapes. auto dec = data_handler_provider.decode(a); decoded_action da{std::move(dec.params), std::move(dec.return_data), std::move(dec.error_message)}; fc::mutable_variant_object av = build_action_variant(a, da, shape); - av("trx_id", trx.id.str()) - ("block_num", trx.block_num) - ("block_time", trx.block_time) - ("producer_block_id", trx.producer_block_id); + av("trx_id", trx_id_str) + ("block_num", trx_block) + ("block_time", trx_time) + ("producer_block_id", trx_pbid); result.actions.emplace_back(std::move(av)); } } From 3bbf9e22e9103b1f605b19bded6be37d6aad7e3f Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 22 Apr 2026 15:49:32 -0500 Subject: [PATCH 25/32] trace_api: per-slice bloom sidecar for get_actions slice-skip Adds trace_recv_bloom_.log per slice containing two boost::bloom filters (K=7, FPR=0.01): one over action_trace_v0::receiver and one over packed (receiver, action) pairs. get_actions consults the bloom once per slice and advances block_num past the slice on a negative probe, turning the "receiver never appears in this slice" case from a 10,000-block scan into an O(1) file read. Pieces: - bloom_sidecar.hpp: header-only bloom_builder + bloom_reader over boost::bloom::filter. On-disk header uses the same uint32 magic convention as blk_offset_index_header (0x42524957 -> "WIRB" on little-endian) with in-class initializers and a reserved pad word for natural alignment. Body is recv bits + recv_action bits + trailing CRC32. Reader rejects bad magic, version mismatches, truncation, and CRC mismatches; on rejection may_contain_* returns true so the caller falls back to a scan (fail-safe - a false negative would silently drop matching actions). - store_provider::append() feeds every block's actions into a per-slice builder and flushes to disk at slice roll-over via temp + rename. A crash between roll-overs leaves the in-progress slice without a bloom; the scan fall-back keeps query correctness at the cost of one scan-only slice until retention ages it out. - request_handler::get_actions_impl opens the bloom lazily per slice, probes the receiver filter (when set) and the (receiver, action) composite (when both are set), and skips the remainder of the slice on a negative probe. Unfiltered queries don't touch the bloom. - slice_directory::run_maintenance_tasks removes trace_recv_bloom_.log alongside the other per-slice files during retention pruning so the sidecar doesn't leak as data is aged out. Uses boost::bloom (Boost 1.89, header-only via vcpkg) and boost::unordered_flat_set, both already in the installed header set. Test coverage (10 new cases, all 107 trace_api_plugin cases pass): - bloom_sidecar_tests: round-trip hits/misses, empty slice produces valid always-miss file, add_block walks every action, missing file / bad magic / CRC corruption / truncation / version mismatch all fail-safe to invalid reader. - get_actions_tests: valid bloom with the queried receiver absent causes the entire slice to be skipped without any get_block call; no-filter query does not consult the bloom. --- .../include/sysio/trace_api/bloom_sidecar.hpp | 211 ++++++++++++++++ .../sysio/trace_api/request_handler.hpp | 37 +++ .../sysio/trace_api/store_provider.hpp | 43 ++++ .../trace_api_plugin/src/store_provider.cpp | 38 +++ .../trace_api_plugin/src/trace_api_plugin.cpp | 12 + plugins/trace_api_plugin/test/CMakeLists.txt | 1 + .../test/test_bloom_sidecar.cpp | 225 ++++++++++++++++++ .../test/test_get_actions.cpp | 93 ++++++++ plugins/trace_api_plugin/trace_api_plugin.md | 106 ++++++++- 9 files changed, 763 insertions(+), 3 deletions(-) create mode 100644 plugins/trace_api_plugin/include/sysio/trace_api/bloom_sidecar.hpp create mode 100644 plugins/trace_api_plugin/test/test_bloom_sidecar.cpp diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/bloom_sidecar.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/bloom_sidecar.hpp new file mode 100644 index 0000000000..3b8e1e2591 --- /dev/null +++ b/plugins/trace_api_plugin/include/sysio/trace_api/bloom_sidecar.hpp @@ -0,0 +1,211 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace sysio::trace_api { + +/// Per-slice bloom sidecar: lets get_actions skip a whole slice when the requested receiver (or receiver+action) is +/// not present anywhere in the slice's action traces. Contains two filters in one file: +/// - receivers: boost::bloom::filter over action_trace_v0::receiver +/// - recv_action: boost::bloom::filter over pack_recv_action(receiver, action) +/// A negative bloom probe is authoritative (skip the slice). A positive probe falls through to the normal scan. +/// Missing or corrupt sidecar => reader is invalid => caller falls back to full scan. +namespace bloom { + +/// File format constants. Stored little-endian on disk so a hex dump of the first 4 bytes reads "WIRB"; matches the +/// convention in blk_offset_index_header and the rest of the trace_api sidecars. Native-endian, x86_64 Linux only. +inline constexpr uint32_t magic_value = 0x42524957; // bytes on disk: 'W','I','R','B' +inline constexpr uint32_t file_version = 1; +inline constexpr uint32_t k_hashes = 7; ///< Fixed at compile time; reader rejects mismatched files. +inline constexpr double target_fpr = 0.01; ///< 1% false-positive rate. Irrelevant for negatives. +inline constexpr uint32_t min_capacity = 32; ///< Floor on filter sizing to avoid degenerate tiny filters. + +/// Raw on-disk header. Body layout: recv bits (recv_capacity_bits/8 bytes) then recv_action bits, trailing uint32 +/// CRC32 over [header | body]. Fields are ordered so natural alignment produces no padding and the layout is +/// stable across compilers; a `reserved` pad word keeps the uint64 fields 8-byte aligned without #pragma pack. +struct header { + uint32_t magic = magic_value; + uint32_t version = file_version; + uint32_t k_hashes = bloom::k_hashes; // qualified to disambiguate from the field name + uint32_t n_recv = 0; ///< Distinct receivers inserted (pre-rounding). + uint32_t n_recv_action = 0; ///< Distinct (receiver, action) pairs inserted. + uint32_t reserved = 0; + uint64_t recv_capacity_bits = 0; ///< Filter capacity in bits; reader constructs filter with this. + uint64_t recv_action_capacity_bits = 0; +}; +static_assert(sizeof(header) == 4 * 6 + 8 * 2, "bloom::header layout drift"); + +/// Deterministic packing of (receiver, action) into one 64-bit key for the composite filter. Rotate receiver to +/// separate it from the action in the bit distribution so distinct (r, a) pairs don't collide on the common +/// receiver==action case. Must match between write and read paths. +inline uint64_t pack_recv_action(chain::name r, chain::name a) noexcept { + const uint64_t rv = r.to_uint64_t(); + const uint64_t av = a.to_uint64_t(); + return ((rv << 13) | (rv >> (64 - 13))) ^ av; +} + +using filter_t = boost::bloom::filter; + +} // namespace bloom + +/// Accumulates distinct receivers and (receiver, action) pairs observed while a slice is being written. Finalize +/// sizes and materializes two blooms and writes the sidecar file atomically (temp + rename). Memory cost is two +/// hash sets keyed on uint64_t; at Wire-mainnet scale these stay a few tens of KB per open slice. +class bloom_builder { +public: + void add_action(const action_trace_v0& a) { + _receivers.insert(a.receiver.to_uint64_t()); + _recv_actions.insert(bloom::pack_recv_action(a.receiver, a.action)); + } + + void add_block(const block_trace_v0& bt) { + for (const auto& trx : bt.transactions) { + for (const auto& act : trx.actions) { + add_action(act); + } + } + } + + /// true when no actions have been fed; finalize_and_write still produces a valid file whose probes always miss. + bool empty() const noexcept { return _receivers.empty() && _recv_actions.empty(); } + + std::size_t receiver_count() const noexcept { return _receivers.size(); } + std::size_t recv_action_count() const noexcept { return _recv_actions.size(); } + + /// Writes to `path + ".tmp"` then renames over `path` to keep the sidecar crash-consistent: either the old file + /// remains intact or the new one is fully installed, never a partial file under the canonical name. + void finalize_and_write(const std::filesystem::path& path) const { + const std::size_t n_recv = std::max(_receivers.size(), bloom::min_capacity); + const std::size_t n_recv_action = std::max(_recv_actions.size(), bloom::min_capacity); + + bloom::filter_t recv_f{n_recv, bloom::target_fpr}; + for (uint64_t v : _receivers) recv_f.insert(v); + + bloom::filter_t ra_f{n_recv_action, bloom::target_fpr}; + for (uint64_t v : _recv_actions) ra_f.insert(v); + + bloom::header hdr{}; + hdr.n_recv = static_cast(_receivers.size()); + hdr.n_recv_action = static_cast(_recv_actions.size()); + hdr.recv_capacity_bits = recv_f.capacity(); + hdr.recv_action_capacity_bits = ra_f.capacity(); + + const auto recv_bits = recv_f.array(); + const auto ra_bits = ra_f.array(); + + boost::crc_32_type crc; + crc.process_bytes(&hdr, sizeof(hdr)); + crc.process_bytes(recv_bits.data(), recv_bits.size()); + crc.process_bytes(ra_bits.data(), ra_bits.size()); + const uint32_t crc_v = crc.checksum(); + + const auto tmp = std::filesystem::path(path).concat(".tmp"); + { + std::ofstream out(tmp, std::ios::binary | std::ios::trunc); + if (!out) throw std::runtime_error("bloom: cannot open " + tmp.string()); + out.write(reinterpret_cast(&hdr), sizeof(hdr)); + out.write(reinterpret_cast(recv_bits.data()), recv_bits.size()); + out.write(reinterpret_cast(ra_bits.data()), ra_bits.size()); + out.write(reinterpret_cast(&crc_v), sizeof(crc_v)); + if (!out) throw std::runtime_error("bloom: write failed for " + tmp.string()); + } + std::filesystem::rename(tmp, path); + } + +private: + boost::unordered_flat_set _receivers; + boost::unordered_flat_set _recv_actions; +}; + +/// Load-time view of a sidecar. Constructor is strict: any failure (missing file, bad magic, version mismatch, +/// truncated body, CRC mismatch) leaves the reader in an invalid state and may_contain_* always returns true. +/// Returning true on invalid means "don't skip" - the correct fail-safe since a false negative would silently drop +/// matching actions from the response. +class bloom_reader { +public: + bloom_reader() = default; + + explicit bloom_reader(const std::filesystem::path& path) { + load(path); + } + + bool valid() const noexcept { return _valid; } + + /// Invariant: on invalid reader, returns true so the caller treats the slice as "may contain, scan". + bool may_contain_receiver(chain::name r) const { + if (!_valid) return true; + return _recv.may_contain(r.to_uint64_t()); + } + + bool may_contain_recv_action(chain::name r, chain::name a) const { + if (!_valid) return true; + return _recv_action.may_contain(bloom::pack_recv_action(r, a)); + } + +private: + void load(const std::filesystem::path& path) { + std::error_code ec; + const auto file_size = std::filesystem::file_size(path, ec); + if (ec || file_size < sizeof(bloom::header) + sizeof(uint32_t)) return; + + std::ifstream in(path, std::ios::binary); + if (!in) return; + + bloom::header hdr; + in.read(reinterpret_cast(&hdr), sizeof(hdr)); + if (!in) return; + if (hdr.magic != bloom::magic_value) return; + if (hdr.version != bloom::file_version) return; + if (hdr.k_hashes != bloom::k_hashes) return; + if (hdr.recv_capacity_bits % CHAR_BIT != 0) return; + if (hdr.recv_action_capacity_bits % CHAR_BIT != 0) return; + + const std::size_t recv_bytes = hdr.recv_capacity_bits / CHAR_BIT; + const std::size_t ra_bytes = hdr.recv_action_capacity_bits / CHAR_BIT; + const std::size_t expected_size = sizeof(bloom::header) + recv_bytes + ra_bytes + sizeof(uint32_t); + if (file_size != expected_size) return; + + std::vector recv_buf(recv_bytes); + std::vector ra_buf(ra_bytes); + in.read(reinterpret_cast(recv_buf.data()), recv_bytes); + in.read(reinterpret_cast(ra_buf.data()), ra_bytes); + uint32_t file_crc = 0; + in.read(reinterpret_cast(&file_crc), sizeof(file_crc)); + if (!in) return; + + boost::crc_32_type crc; + crc.process_bytes(&hdr, sizeof(hdr)); + crc.process_bytes(recv_buf.data(), recv_buf.size()); + crc.process_bytes(ra_buf.data(), ra_buf.size()); + if (crc.checksum() != file_crc) return; + + bloom::filter_t recv_f{hdr.recv_capacity_bits}; + bloom::filter_t ra_f{hdr.recv_action_capacity_bits}; + // filter::array() returns a boost::span over the filter's backing byte array. capacity() already matched the + // header's capacity on construction, so the span sizes match our buffers and the bitwise copy is well defined. + if (recv_f.array().size() != recv_buf.size()) return; + if (ra_f.array().size() != ra_buf.size()) return; + std::memcpy(recv_f.array().data(), recv_buf.data(), recv_buf.size()); + std::memcpy(ra_f.array().data(), ra_buf.data(), ra_buf.size()); + + _recv = std::move(recv_f); + _recv_action = std::move(ra_f); + _valid = true; + } + + bool _valid = false; + bloom::filter_t _recv; + bloom::filter_t _recv_action; +}; + +} // namespace sysio::trace_api diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp index 3cd46df931..a158b4c8ee 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -199,8 +200,44 @@ namespace sysio::trace_api { // trxs with similar action counts avoid per-trx allocations. std::vector matches; + // Per-slice bloom skip state. When the caller supplies a receiver (or a non-include_notifications + // request whose receiver is auto-mirrored onto account upstream), probe the slice's receiver bloom and + // advance block_num past the slice on a negative probe. The bloom is opened once per slice (lazy; only + // if skipping is useful for this query) and held for the life of the scan through that slice. If the + // sidecar is missing or CRC-corrupt the bloom_reader is invalid and may_contain_* returns true, which + // preserves the existing scan behaviour. + const uint32_t stride = logfile_provider.slice_stride(); + const bool skip_eligible = has_receiver || (has_account && has_action); + std::optional current_slice; + bool skip_current_slice = false; + const uint32_t end = query.block_num_end; for (uint32_t block_num = query.block_num_start; block_num <= end; ++block_num) { + if (skip_eligible) { + const uint32_t slice = logfile_provider.slice_number(block_num); + if (!current_slice || *current_slice != slice) { + current_slice = slice; + skip_current_slice = false; + bloom_reader r = logfile_provider.get_bloom(slice); + if (r.valid()) { + if (has_receiver && !r.may_contain_receiver(receiver_v)) { + skip_current_slice = true; + } else if (has_receiver && has_action + && !r.may_contain_recv_action(receiver_v, action_v)) { + skip_current_slice = true; + } + } + } + if (skip_current_slice) { + // Jump block_num to the last block of this slice so the for-loop's ++block_num takes us to the + // first block of the next slice. Clamp to the query's end so we don't wrap around if this is + // the last slice in the range. + const uint32_t slice_last = (slice + 1) * stride - 1; + block_num = std::min(slice_last, end); + continue; + } + } + auto data = logfile_provider.get_block(block_num); if (!data) continue; diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp index 5b3aebdb97..2de91a9f9f 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -137,6 +138,18 @@ namespace sysio::trace_api { return block_height / _width; } + /** + * Slice stride (blocks per slice) as configured at construction. + */ + uint32_t width() const noexcept { return _width; } + + /** + * Filesystem path for a slice's receiver bloom sidecar. The file is only read/written via the bloom_builder + * and bloom_reader helpers; no fc::cfile overload is provided because the sidecar is written once at slice + * close (temp + rename) and only mmap-style read by the query path. + */ + std::filesystem::path bloom_slice_path(uint32_t slice_number) const; + /** * Find or create the index file associated with the indicated slice_number * @@ -358,6 +371,26 @@ namespace sysio::trace_api { void append_lib(uint32_t lib); void append_trx_ids(block_trxs_entry tt); + /** + * Slice stride used for all sidecars. Exposed on the provider so callers (e.g. request_handler's block-range + * scan) can partition queries by slice without having to reach into slice_directory. + */ + uint32_t slice_stride() const noexcept { return _slice_directory.width(); } + + /** + * Slice number containing the given block. + */ + uint32_t slice_number(uint32_t block_height) const noexcept { return _slice_directory.slice_number(block_height); } + + /** + * Open the per-slice bloom sidecar for a given slice number. Returns a bloom_reader whose valid() is false + * when the sidecar is missing, truncated, wrong-version, or CRC-corrupt - in which case the caller MUST fall + * back to a full scan of the slice (an invalid reader returns true from may_contain_*, honoring the fail-safe + * invariant). A positive probe is not authoritative (standard bloom semantics); only a negative probe on a + * valid reader permits skipping. + */ + bloom_reader get_bloom(uint32_t slice_number) const; + /** * Record an ABI version for an account at a given global_sequence. * global_seq == 0 means "captured lazily; exact seq unknown". @@ -498,6 +531,16 @@ namespace sysio::trace_api { // ABI sidecar: one global append-only log in the slice directory. // abi_log serialises its own writes and allows concurrent lookups. abi_log _abi_log; + + // Per-slice bloom sidecar writer state. Extraction is single-threaded (chain signal thread), so the builder + // and slice-number tracker are plain members - no locking needed. The builder accumulates receivers and + // (receiver, action) pairs observed within the currently-open slice; append() detects a slice roll-over by + // comparing the incoming block's slice number against _current_bloom_slice, and on roll-over writes the + // completed slice's bloom to disk before resetting state for the new slice. If the node crashes before a + // roll-over fires, the in-flight slice's bloom is lost; request_handler treats a missing sidecar as + // "scan the slice" (fail-safe), which keeps query results correct at the cost of that one slice being scanned. + std::optional _current_bloom_slice; + bloom_builder _current_bloom_builder; }; } diff --git a/plugins/trace_api_plugin/src/store_provider.cpp b/plugins/trace_api_plugin/src/store_provider.cpp index 7b1e010c0d..7145fac71d 100644 --- a/plugins/trace_api_plugin/src/store_provider.cpp +++ b/plugins/trace_api_plugin/src/store_provider.cpp @@ -12,6 +12,7 @@ namespace { static constexpr const char* _trace_trx_id_prefix = "trace_trx_id_"; static constexpr const char* _trace_trx_id_index_prefix = "trace_trx_idx_"; static constexpr const char* _trace_blk_idx_prefix = "trace_blk_idx_"; + static constexpr const char* _trace_recv_bloom_prefix = "trace_recv_bloom_"; static constexpr const char* _trace_ext = ".log"; static constexpr const char* _compressed_trace_ext = ".clog"; // Sized for the longest possible filename across every prefix and @@ -23,6 +24,7 @@ namespace { std::char_traits::length(_trace_trx_id_prefix), std::char_traits::length(_trace_trx_id_index_prefix), std::char_traits::length(_trace_blk_idx_prefix), + std::char_traits::length(_trace_recv_bloom_prefix), }); static constexpr size_t _max_ext_length = std::max( std::char_traits::length(_trace_ext), @@ -59,6 +61,22 @@ namespace sysio::trace_api { fc::cfile trace; fc::cfile index; const uint32_t slice_number = _slice_directory.slice_number(bt.number); + + // Detect slice roll-over and flush the completed slice's bloom sidecar before we start accumulating into the + // new slice's builder. A failure here is logged by the general except_handler path (finalize_and_write throws + // on I/O error); the sidecar is advisory so a missing one just means the query path falls back to scanning + // that slice. We swallow the write error here to avoid tying slice roll-over durability to bloom durability. + if (_current_bloom_slice && *_current_bloom_slice != slice_number) { + try { + _current_bloom_builder.finalize_and_write(_slice_directory.bloom_slice_path(*_current_bloom_slice)); + } catch (const std::exception& e) { + fc_wlog(_log, "trace_api: failed to write bloom sidecar for slice {}: {}", *_current_bloom_slice, e.what()); + } + _current_bloom_builder = bloom_builder{}; + } + _current_bloom_slice = slice_number; + _current_bloom_builder.add_block(bt); + _slice_directory.find_or_create_slice_pair(slice_number, open_state::write, trace, index); // storing as static_variant to allow adding other data types to the trace file in the future const uint64_t offset = append_store(data_log_entry { bt }, trace); @@ -93,6 +111,13 @@ namespace sysio::trace_api { append_store(entry, trx_id_file); } + bloom_reader store_provider::get_bloom(uint32_t slice_number) const { + const auto path = _slice_directory.bloom_slice_path(slice_number); + std::error_code ec; + if (!std::filesystem::exists(path, ec)) return bloom_reader{}; + return bloom_reader{path}; + } + get_block_t store_provider::get_block(uint32_t block_height, const yield_function& yield) { // Fast path: O(1) random-access lookup of the trace offset via the block-offset sidecar. std::optional trace_offset = _slice_directory.lookup_block_offset(block_height); @@ -298,6 +323,13 @@ namespace sysio::trace_api { return true; } + std::filesystem::path slice_directory::bloom_slice_path(uint32_t slice_number) const { + // Mirrors the filename convention of the other sidecars: -. Callers write through + // bloom_builder::finalize_and_write (temp + rename) and read through bloom_reader, neither of which uses + // fc::cfile, so no open-helper is needed here. + return _slice_dir / make_filename(_trace_recv_bloom_prefix, _trace_ext, slice_number, _width); + } + std::optional slice_directory::find_compressed_trace_slice(uint32_t slice_number, bool open_file ) const { auto filename = make_filename(_trace_prefix, _compressed_trace_ext, slice_number, _width); const auto slice_path = _slice_dir / filename; @@ -766,6 +798,12 @@ namespace sysio::trace_api { std::filesystem::remove(blk_idx_path); } + const auto bloom_path = bloom_slice_path(slice_to_clean); + if (std::filesystem::exists(bloom_path)) { + log(std::string("Removing: ") + bloom_path.generic_string()); + std::filesystem::remove(bloom_path); + } + auto ctrace = find_compressed_trace_slice(slice_to_clean, dont_open_file); if (ctrace) { log(std::string("Removing: ") + ctrace->get_file_path().generic_string()); diff --git a/plugins/trace_api_plugin/src/trace_api_plugin.cpp b/plugins/trace_api_plugin/src/trace_api_plugin.cpp index 64df50bc40..88046b8db0 100644 --- a/plugins/trace_api_plugin/src/trace_api_plugin.cpp +++ b/plugins/trace_api_plugin/src/trace_api_plugin.cpp @@ -106,6 +106,18 @@ namespace { return store->has_abi_entry(account); } + uint32_t slice_stride() const noexcept { + return store->slice_stride(); + } + + uint32_t slice_number(uint32_t block_height) const noexcept { + return store->slice_number(block_height); + } + + bloom_reader get_bloom(uint32_t slice_number) const { + return store->get_bloom(slice_number); + } + std::shared_ptr store; }; } diff --git a/plugins/trace_api_plugin/test/CMakeLists.txt b/plugins/trace_api_plugin/test/CMakeLists.txt index f5615fef49..60b34afef8 100644 --- a/plugins/trace_api_plugin/test/CMakeLists.txt +++ b/plugins/trace_api_plugin/test/CMakeLists.txt @@ -9,6 +9,7 @@ add_executable( test_trace_api_plugin test_trx_id_index.cpp test_abi_log.cpp test_get_actions.cpp + test_bloom_sidecar.cpp main.cpp ) target_link_libraries( test_trace_api_plugin trace_api_plugin ) diff --git a/plugins/trace_api_plugin/test/test_bloom_sidecar.cpp b/plugins/trace_api_plugin/test/test_bloom_sidecar.cpp new file mode 100644 index 0000000000..71306cc3dd --- /dev/null +++ b/plugins/trace_api_plugin/test/test_bloom_sidecar.cpp @@ -0,0 +1,225 @@ +#include +#include + +#include +#include + +#include +#include +#include + +using namespace sysio; +using namespace sysio::trace_api; +using sysio::chain::name; +using sysio::chain::operator""_n; + +namespace { + +/// Build a minimal action_trace_v0 with only the fields the bloom consumes. +action_trace_v0 act(name receiver, name account, name action) { + action_trace_v0 a{}; + a.receiver = receiver; + a.account = account; + a.action = action; + return a; +} + +/// Pack three actions into a single-transaction block_trace so tests can exercise add_block without pulling in the +/// full extraction machinery. Field defaults are fine - bloom_builder only reads receiver/account/action. +block_trace_v0 block_with(std::vector actions) { + transaction_trace_v0 t{}; + t.actions = std::move(actions); + block_trace_v0 bt{}; + bt.transactions.push_back(std::move(t)); + return bt; +} + +} // namespace + +BOOST_AUTO_TEST_SUITE(bloom_sidecar_tests) + +/// A slice with a known set of receivers yields hits for every inserted receiver and (receiver, action) pair, and +/// misses for names not inserted. The miss rate for well-separated unknowns should be at or below the target FPR; +/// with 32 inserted items at p=0.01 we expect roughly zero false positives on the 8 probe names we test. +BOOST_AUTO_TEST_CASE(roundtrip_hits_and_misses) { + fc::temp_directory tempdir; + const auto path = tempdir.path() / "bloom_roundtrip.log"; + + const std::vector present = { "alice"_n, "bob"_n, "charlie"_n, "sysio.token"_n }; + const std::vector absent = { "dave"_n, "eve"_n, "unknown.acc"_n, "never.seen"_n }; + const name transfer = "transfer"_n; + const name setabi = "setabi"_n; + + bloom_builder b; + for (auto r : present) { + b.add_action(act(r, r, transfer)); + } + BOOST_REQUIRE_EQUAL(b.receiver_count(), present.size()); + BOOST_REQUIRE_EQUAL(b.recv_action_count(), present.size()); + b.finalize_and_write(path); + + bloom_reader r(path); + BOOST_REQUIRE(r.valid()); + + for (auto rcv : present) { + BOOST_TEST_INFO("present receiver " << rcv.to_string()); + BOOST_CHECK(r.may_contain_receiver(rcv)); + BOOST_CHECK(r.may_contain_recv_action(rcv, transfer)); + } + + std::size_t false_positives = 0; + for (auto rcv : absent) { + if (r.may_contain_receiver(rcv)) ++false_positives; + } + BOOST_CHECK_LE(false_positives, 1u); + + // A (receiver, action) probe with a present receiver but unseen action should almost always miss, since the + // composite key has more entropy than receiver alone. + std::size_t composite_fp = 0; + for (auto rcv : present) { + if (r.may_contain_recv_action(rcv, setabi)) ++composite_fp; + } + BOOST_CHECK_LE(composite_fp, 1u); +} + +/// Building from an empty slice still produces a valid file; probes always miss. This is the "no transactions in +/// any block of the slice" case. +BOOST_AUTO_TEST_CASE(empty_builder_produces_valid_file_all_miss) { + fc::temp_directory tempdir; + const auto path = tempdir.path() / "bloom_empty.log"; + + bloom_builder b; + BOOST_REQUIRE(b.empty()); + b.finalize_and_write(path); + + bloom_reader r(path); + BOOST_REQUIRE(r.valid()); + + for (auto n : { "alice"_n, "bob"_n, "sysio.token"_n, "anything"_n }) { + BOOST_CHECK(!r.may_contain_receiver(n)); + BOOST_CHECK(!r.may_contain_recv_action(n, "transfer"_n)); + } +} + +/// add_block feeds every action_trace in every transaction into both filters. +BOOST_AUTO_TEST_CASE(add_block_walks_all_transactions) { + fc::temp_directory tempdir; + const auto path = tempdir.path() / "bloom_block.log"; + + bloom_builder b; + const auto bt = block_with({ + act("alice"_n, "sysio.token"_n, "transfer"_n), + act("sysio.token"_n, "sysio.token"_n, "transfer"_n), + act("bob"_n, "sysio.token"_n, "transfer"_n), + }); + b.add_block(bt); + b.finalize_and_write(path); + + bloom_reader r(path); + BOOST_REQUIRE(r.valid()); + BOOST_CHECK(r.may_contain_receiver("alice"_n)); + BOOST_CHECK(r.may_contain_receiver("bob"_n)); + BOOST_CHECK(r.may_contain_receiver("sysio.token"_n)); + BOOST_CHECK(r.may_contain_recv_action("alice"_n, "transfer"_n)); + BOOST_CHECK(r.may_contain_recv_action("bob"_n, "transfer"_n)); +} + +/// Missing file: reader is invalid and probes default to true so the caller falls back to scanning the slice +/// instead of silently dropping matches. +BOOST_AUTO_TEST_CASE(missing_file_is_invalid_fail_safe_true) { + fc::temp_directory tempdir; + const auto path = tempdir.path() / "does_not_exist.log"; + + bloom_reader r(path); + BOOST_CHECK(!r.valid()); + BOOST_CHECK(r.may_contain_receiver("alice"_n)); + BOOST_CHECK(r.may_contain_recv_action("alice"_n, "transfer"_n)); +} + +/// Bad magic is rejected. Overwrites the first byte of a freshly built file. +BOOST_AUTO_TEST_CASE(bad_magic_is_invalid) { + fc::temp_directory tempdir; + const auto path = tempdir.path() / "bloom_badmagic.log"; + + bloom_builder b; + b.add_action(act("alice"_n, "sysio.token"_n, "transfer"_n)); + b.finalize_and_write(path); + + { + std::fstream f(path.string(), std::ios::binary | std::ios::in | std::ios::out); + f.seekp(0); + char bad = 'X'; + f.write(&bad, 1); + } + + bloom_reader r(path); + BOOST_CHECK(!r.valid()); +} + +/// A single flipped bit in the body invalidates the CRC, and the reader rejects the file rather than trusting +/// potentially corrupted filter state. +BOOST_AUTO_TEST_CASE(corrupted_body_rejected_by_crc) { + fc::temp_directory tempdir; + const auto path = tempdir.path() / "bloom_crc.log"; + + bloom_builder b; + b.add_action(act("alice"_n, "sysio.token"_n, "transfer"_n)); + b.finalize_and_write(path); + + // Flip a bit inside the first receiver-bloom byte (well after the header, well before the trailing CRC). + const auto hdr_size = sizeof(bloom::header); + { + std::fstream f(path.string(), std::ios::binary | std::ios::in | std::ios::out); + f.seekg(hdr_size); + char b0 = 0; + f.read(&b0, 1); + b0 ^= 0x01; + f.seekp(hdr_size); + f.write(&b0, 1); + } + + bloom_reader r(path); + BOOST_CHECK(!r.valid()); +} + +/// Truncated file: drop the last few bytes. Reader detects the size mismatch and rejects the file. +BOOST_AUTO_TEST_CASE(truncated_file_rejected) { + fc::temp_directory tempdir; + const auto path = tempdir.path() / "bloom_trunc.log"; + + bloom_builder b; + b.add_action(act("alice"_n, "sysio.token"_n, "transfer"_n)); + b.finalize_and_write(path); + + const auto full_size = std::filesystem::file_size(path); + BOOST_REQUIRE(full_size > 8); + std::filesystem::resize_file(path, full_size - 8); + + bloom_reader r(path); + BOOST_CHECK(!r.valid()); +} + +/// File written with a different bloom version number is rejected. Prevents silently mis-probing a future-format +/// file. +BOOST_AUTO_TEST_CASE(version_mismatch_rejected) { + fc::temp_directory tempdir; + const auto path = tempdir.path() / "bloom_ver.log"; + + bloom_builder b; + b.add_action(act("alice"_n, "sysio.token"_n, "transfer"_n)); + b.finalize_and_write(path); + + // Rewrite the version field to a future value. The reader compares against bloom::file_version and the CRC + // recomputation will not rescue the file either, but version gate fires first. + { + std::fstream f(path.string(), std::ios::binary | std::ios::in | std::ios::out); + f.seekp(offsetof(bloom::header, version)); + uint32_t bumped = bloom::file_version + 1; + f.write(reinterpret_cast(&bumped), sizeof(bumped)); + } + + bloom_reader r(path); + BOOST_CHECK(!r.valid()); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/plugins/trace_api_plugin/test/test_get_actions.cpp b/plugins/trace_api_plugin/test/test_get_actions.cpp index 8c4027638e..885357fcc2 100644 --- a/plugins/trace_api_plugin/test/test_get_actions.cpp +++ b/plugins/trace_api_plugin/test/test_get_actions.cpp @@ -1,6 +1,9 @@ #include +#include + #include +#include #include #include @@ -24,6 +27,17 @@ struct get_actions_fixture { return std::make_tuple(data_log_entry{it->second}, true /*irreversible*/); } + // Stride/slice mapping is a fixture knob so tests can exercise the per-slice bloom skip path with a small + // stride rather than the production default of 10,000 blocks. + uint32_t slice_stride() const noexcept { return fixture.mock_slice_stride; } + uint32_t slice_number(uint32_t block_num) const noexcept { return block_num / fixture.mock_slice_stride; } + + // Default: no sidecar -> invalid bloom_reader -> may_contain_* returns true -> caller scans as before. Tests + // that want to exercise skipping install a function that returns a valid reader for specific slices. + bloom_reader get_bloom(uint32_t slice_number) const { + return fixture.mock_get_bloom(slice_number); + } + get_actions_fixture& fixture; }; @@ -70,6 +84,8 @@ struct get_actions_fixture { }; std::map blocks; + uint32_t mock_slice_stride = 10; + std::function mock_get_bloom = [](uint32_t) { return bloom_reader{}; }; impl_type impl; }; @@ -440,4 +456,81 @@ BOOST_FIXTURE_TEST_CASE(complex_inline_and_notification_ordering, get_actions_fi } } +// Per-slice bloom skip: a valid bloom that does not contain the queried receiver causes get_actions_impl to advance +// past the entire slice without scanning any of its blocks. The fixture observes "no scan" by having get_block +// return a single well-known action in every block; if the scan ran, the result would include that action. +BOOST_FIXTURE_TEST_CASE(bloom_skips_entire_slice_when_receiver_absent, get_actions_fixture) { + fc::temp_directory tempdir; + + // Three slices of 10 blocks each; populate every block so a non-skipped scan would always find the single action. + mock_slice_stride = 10; + for (uint32_t n = 1; n < 30; ++n) { + blocks[n] = make_block(n, { make_trx(TRX1, n, { make_action(n, "alice"_n, "alice"_n, "transfer"_n) }) }); + } + + // Build bloom sidecars for slices 0, 1, 2. Slice 1 is the only one that contains alice; slices 0 and 2 have no + // receivers at all (empty blooms -> every probe misses). + auto bloom_for = [&tempdir](std::size_t idx, bool with_alice) { + bloom_builder b; + if (with_alice) { + action_trace_v0 a{}; + a.receiver = "alice"_n; + a.account = "alice"_n; + a.action = "transfer"_n; + b.add_action(a); + } + const auto path = tempdir.path() / ("bloom_slice_" + std::to_string(idx) + ".log"); + b.finalize_and_write(path); + return path; + }; + const auto slice0_path = bloom_for(0, /*with_alice=*/false); + const auto slice1_path = bloom_for(1, /*with_alice=*/true); + const auto slice2_path = bloom_for(2, /*with_alice=*/false); + + mock_get_bloom = [slice0_path, slice1_path, slice2_path](uint32_t slice) -> bloom_reader { + switch (slice) { + case 0: return bloom_reader{slice0_path}; + case 1: return bloom_reader{slice1_path}; + case 2: return bloom_reader{slice2_path}; + default: return bloom_reader{}; + } + }; + + action_query q; + q.block_num_start = 1; + q.block_num_end = 29; + q.receiver = "alice"_n; + + auto r = get_actions(q); + + // All hits come from slice 1 (blocks 10..19). Slices 0 and 2 were bloom-skipped without any get_block call. + BOOST_REQUIRE_EQUAL(r.actions.size(), 10u); + for (const auto& a : r.actions) { + const auto block_num = a.get_object()["block_num"].as_uint64(); + BOOST_TEST(block_num >= 10u); + BOOST_TEST(block_num <= 19u); + } +} + +// Sanity check that a query with no filter cannot bloom-skip: even if the mock would return an empty bloom for +// every slice, we still scan because there's nothing to probe against. Without this behaviour callers would see +// empty results on unfiltered queries once sidecars exist. +BOOST_FIXTURE_TEST_CASE(bloom_not_consulted_when_no_filter, get_actions_fixture) { + mock_slice_stride = 10; + for (uint32_t n = 1; n < 15; ++n) { + blocks[n] = make_block(n, { make_trx(TRX1, n, { make_action(n, "alice"_n, "alice"_n, "transfer"_n) }) }); + } + // If the handler ever calls get_bloom under this configuration, fail loudly. + mock_get_bloom = [](uint32_t) -> bloom_reader { + BOOST_FAIL("get_bloom should not be called when no filter is set"); + return bloom_reader{}; + }; + + action_query q; + q.block_num_start = 1; + q.block_num_end = 14; + auto r = get_actions(q); + BOOST_CHECK_EQUAL(r.actions.size(), 14u); +} + BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/plugins/trace_api_plugin/trace_api_plugin.md b/plugins/trace_api_plugin/trace_api_plugin.md index a345ddf991..d3c90be09d 100644 --- a/plugins/trace_api_plugin/trace_api_plugin.md +++ b/plugins/trace_api_plugin/trace_api_plugin.md @@ -79,7 +79,7 @@ default). | Option | Default | Description | |--------|---------|-------------| | `trace-dir` | `traces` | Directory for trace files. Relative paths are resolved from the node's data directory. | -| `trace-slice-stride` | `10000` | Number of blocks per slice file. Must be in `[1, 1000000]`. Larger values reduce file count but bloat the block-offset sidecar's per-slice pre-allocation (`stride * 8` bytes, sparse) and stress the per-slice trx_id hash index (rejected if it would need more than 2^28 buckets). | +| `trace-slice-stride` | `10000` | Number of blocks per slice file. Must be in `[1, 1000000]`. Larger values reduce file count but bloat the block-offset sidecar's per-slice pre-allocation (`stride * 8` bytes, sparse) and stress the per-slice trx_id hash index (rejected if it would need more than 2^28 buckets). Also bounds the worst-case scan cost of `get_actions` on a positive bloom probe - smaller strides mean less work per hit-slice at the cost of more sidecar files and more frequent slice roll-overs (see [Slice stride vs. query latency](#slice-stride-vs-query-latency)). Setting takes effect on nodeop restart; existing slices retain their old naming. | | `trace-minimum-irreversible-history-blocks` | `-1` | Blocks past LIB to retain before old slices can be auto-deleted. `-1` disables automatic deletion (keep forever). | | `trace-minimum-uncompressed-irreversible-history-blocks` | `-1` | Blocks past LIB to keep uncompressed. Slices older than this threshold are transparently compressed. `-1` disables automatic compression. | | `trace-max-block-range` | `1000` | Maximum number of blocks scanned by a single `get_actions` or `get_token_transfers` request. Must be in `[1, 10000]`. `block_num_end` is silently clamped to `block_num_start + trace-max-block-range - 1` when a request asks for more. The response envelope always reports the actual range scanned. | @@ -694,7 +694,7 @@ All files live inside `trace-dir`. The directory is monitored by #### Slice files Blocks are grouped into contiguous slices of `trace-slice-stride` blocks -each. Each slice is represented by four files that share a common range +each. Each slice is represented by five files that share a common range suffix `-` (zero-padded to 10 digits): | File | Description | @@ -703,6 +703,7 @@ suffix `-` (zero-padded to 10 digits): | `trace_index_-.log` | Append-only metadata log of `block_entry_v0` and `lib_entry_v0` records. Source of truth; used as a fallback for `get_block` and to track LIB advancement within the slice. | | `trace_blk_idx_-.log` | Block-offset sidecar. Enables O(1) `get_block` lookups regardless of the block's position within the slice. | | `trace_trx_idx_-.log` | Transaction-id hash index. | +| `trace_recv_bloom_-.log` | Per-slice bloom filter over action receivers and (receiver, action) pairs. `get_actions` consults it to skip slices that cannot contain the requested filter value. | When a slice is compressed the trace file is replaced by: @@ -710,7 +711,7 @@ When a slice is compressed the trace file is replaced by: |------|-------------| | `trace_-.clog` | zlib-compressed trace data with embedded seek points for random access. | -The index and trx_id index files are not compressed. +The metadata, block-offset, trx-id, and receiver-bloom sidecars are not compressed - they are already compact and need random access. **Example** (10 000-block stride, blocks 0–29 999): @@ -720,14 +721,17 @@ traces/ trace_index_0000000000-0000010000.log trace_blk_idx_0000000000-0000010000.log trace_trx_idx_0000000000-0000010000.log + trace_recv_bloom_0000000000-0000010000.log trace_0000010000-0000020000.log trace_index_0000010000-0000020000.log trace_blk_idx_0000010000-0000020000.log trace_trx_idx_0000010000-0000020000.log + trace_recv_bloom_0000010000-0000020000.log trace_0000020000-0000030000.clog <- compressed trace_index_0000020000-0000030000.log trace_blk_idx_0000020000-0000030000.log trace_trx_idx_0000020000-0000030000.log + trace_recv_bloom_0000020000-0000030000.log abi_log.log ``` @@ -764,6 +768,102 @@ last block becomes irreversible. Queries against `/v1/trace_api/get_transaction_trace` use this index for O(1) `trx_id → block_num` resolution. +#### Receiver bloom sidecar + +`trace_recv_bloom_-.log` is a per-slice pair of bloom +filters used by `/v1/trace_api/get_actions` to skip slices that cannot +contain the requested filter value. Without it, a request for a +rarely-active receiver across a wide block range would have to fetch +and scan every block in every slice; with it, slices that do not +contain the receiver are dismissed by a single O(1) probe and the +scanner advances `block_num` to the next slice boundary. + +Contents: + +- **Receiver filter** - `boost::bloom::filter` over + `action_trace_v0::receiver` (name stored as its 64-bit value). +- **Composite filter** - `boost::bloom::filter` over a + deterministic pack of `(receiver, action)` pairs. Probed when the + caller supplies both a `receiver` and an `action` filter, giving an + extra selectivity win for `get_token_transfers`-style lookups. + +Format: + +``` +Header (40 bytes): + magic (u32) = 0x42524957 ("WIRB" on little-endian) + version (u32) = 1 + k_hashes (u32) = 7 + n_recv (u32) - distinct receivers inserted + n_recv_action (u32) - distinct (receiver, action) pairs inserted + reserved (u32) + recv_capacity_bits (u64) - filter bit count + recv_action_capacity_bits (u64) +Body: + receiver bloom bits (recv_capacity_bits / 8 bytes) + (receiver, action) bits (recv_action_capacity_bits / 8 bytes) + crc32 (u32) over header + body +``` + +Filters are sized for a 1% false-positive rate with a minimum floor of +32 items to avoid degenerate tiny bit arrays on sparse slices. Total +sidecar size is dominated by the number of distinct receivers seen in +the slice (the composite filter scales with (receiver, action) pairs, +typically 2-3x the receiver count). A busy mainnet slice sits around +10 KB; an empty slice produces a minimal always-miss file. + +Build model: populated live during block extraction into two +`boost::unordered_flat_set` accumulators per open slice; +finalized and written (temp + rename) when `append()` detects a slice +roll-over. A node crash between roll-overs leaves the in-progress +slice without a sidecar; `get_actions` then scans that slice as if the +bloom were missing. + +Query model: `get_actions` probes the bloom once per distinct slice in +the queried block range. A negative probe is authoritative and the +entire slice is skipped with no `get_block` call. A positive probe +(or a missing/corrupt sidecar) falls through to the existing scan. +Unfiltered queries (no `receiver`, no `account`, no `action`) do not +consult the bloom. + +Retention: `slice_directory::run_maintenance_tasks` removes the bloom +sidecar alongside the slice's other files when the slice ages out of +`minimum_irreversible_history_blocks`. + +##### Slice stride vs. query latency + +The bloom is per-slice, so on a **positive** probe the scanner still +reads every block in the slice before returning. That cost is bounded +by `trace-slice-stride`: larger strides mean more work per hit-slice, +smaller strides mean finer skip granularity at the cost of more +sidecar files and more frequent slice roll-overs. + +Rough per-slice scan cost on SSD with slices in the OS page cache +(dominated by deserializing `block_trace_v0` records): + +| `trace-slice-stride` | Scan after bloom hit (warm) | Scan (compressed, cold) | Files per slice | +|----------------------|-----------------------------|--------------------------|-----------------| +| 10000 (default) | ~50-100 ms | ~200-500 ms | 5 (+ .clog) | +| 2500 | ~15-25 ms | ~50-125 ms | 5 (+ .clog) | +| 1000 | ~5-10 ms | ~20-50 ms | 5 (+ .clog) | + +Smaller strides approximate a finer-grained bloom skip (bigger +stride-shrink == more precise "miss" resolution) at the cost of +linearly more files on disk. At stride 1000 a year of busy-chain +history lands around 380 000 slice files total, which modern +filesystems handle but makes directory listings slow. + +Queries that cluster near head (the common case) visit only a handful +of slices regardless of stride, so stride mostly affects deep-history +lookups on sparse accounts. For nodes that primarily serve recent +queries the default is fine; nodes that expect frequent deep scans can +benefit from dropping to 2500 or lower. + +`trace-slice-stride` takes effect on nodeop restart. Existing slices +keep their old `-` naming; new slices written after the +restart use the new stride. Nothing is migrated - query paths read +whatever sidecars exist for the slice covering a given block. + #### ABI log `abi_log.log` is an append-only file that persists the ABI published by From 174cf85373c0b09e1cec7a033bce39ed46a8f370 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 22 Apr 2026 17:12:51 -0500 Subject: [PATCH 26/32] trace_api: bloom sidecar review fixes - cfile port, field renames, defensive cap Batch of mechanical + tightening changes from pre-PR review (items 2, 3, 4, 7, 8, 9, 10, 14): - get_actions_impl: tighten skip_eligible to `has_receiver` since both bloom probes require a receiver. Previously `has_account && has_action` could open the sidecar for no probe benefit on `include_notifications = true` queries. - get_actions_impl: rename local filter values receiver_v/account_v/action_v to *_name for readability (matches chain::name type; no "_v suffix" convention elsewhere in the codebase). - bloom_sidecar: rename struct field `k_hashes` to `k_hash_count` so it no longer shadows the namespace constant bloom::k_hashes; removes the disambiguating-qualification wart. - bloom_sidecar: add `max_capacity_bits = 128 MiB` defensive bound in bloom_reader::load. A corrupted or maliciously-crafted sidecar with an absurd capacity could previously trigger a huge std::vector allocation; this caps allocations at a realistic maximum (~500x a busy-mainnet slice bloom). - bloom_sidecar: port bloom_builder::finalize_and_write and bloom_reader::load from std::ifstream/std::ofstream to fc::cfile for consistency with the rest of the plugin's sidecars. Reader catches std::exception broadly so any fc::cfile failure (open, read) still falls back to the scan path via _valid == false. - bloom_sidecar: add `noexcept` to may_contain_receiver and may_contain_recv_action. Both are const, pure-compute, and the invalid-path is a plain return; annotating lets the compiler elide exception-handling metadata in the get_actions inner loop. - bloom_sidecar: comment cleanup (`=>` -> `->`). New test: - bloom_sidecar_tests/filter_capacity_roundtrip_invariant pins the boost::bloom guarantee `filter{f.capacity()}.capacity() == f.capacity()` (documented in boost/bloom/detail/core.hpp:480) across item counts from 1 to 1000. A future boost upgrade that quietly breaks this would fail the test rather than silently disabling the skip path in production. All 108 trace_api_plugin tests pass. --- .../include/sysio/trace_api/bloom_sidecar.hpp | 126 ++++++++++-------- .../sysio/trace_api/request_handler.hpp | 29 ++-- .../test/test_bloom_sidecar.cpp | 41 ++++++ 3 files changed, 128 insertions(+), 68 deletions(-) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/bloom_sidecar.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/bloom_sidecar.hpp index 3b8e1e2591..315352e121 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/bloom_sidecar.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/bloom_sidecar.hpp @@ -3,13 +3,13 @@ #include #include #include +#include #include #include #include #include #include -#include #include namespace sysio::trace_api { @@ -19,7 +19,7 @@ namespace sysio::trace_api { /// - receivers: boost::bloom::filter over action_trace_v0::receiver /// - recv_action: boost::bloom::filter over pack_recv_action(receiver, action) /// A negative bloom probe is authoritative (skip the slice). A positive probe falls through to the normal scan. -/// Missing or corrupt sidecar => reader is invalid => caller falls back to full scan. +/// Missing or corrupt sidecar -> reader is invalid -> caller falls back to full scan. namespace bloom { /// File format constants. Stored little-endian on disk so a hex dump of the first 4 bytes reads "WIRB"; matches the @@ -29,6 +29,10 @@ inline constexpr uint32_t file_version = 1; inline constexpr uint32_t k_hashes = 7; ///< Fixed at compile time; reader rejects mismatched files. inline constexpr double target_fpr = 0.01; ///< 1% false-positive rate. Irrelevant for negatives. inline constexpr uint32_t min_capacity = 32; ///< Floor on filter sizing to avoid degenerate tiny filters. +/// Defensive upper bound on per-filter bit count in a loaded sidecar - a corrupted or maliciously-crafted file with +/// an absurd capacity value would otherwise trigger a huge std::vector allocation. 128 MiB per filter is ~500x the +/// size of a realistic busy-mainnet slice bloom, so no legitimate file hits it. +inline constexpr uint64_t max_capacity_bits = 128ull * 1024 * 1024 * 8; /// Raw on-disk header. Body layout: recv bits (recv_capacity_bits/8 bytes) then recv_action bits, trailing uint32 /// CRC32 over [header | body]. Fields are ordered so natural alignment produces no padding and the layout is @@ -36,7 +40,7 @@ inline constexpr uint32_t min_capacity = 32; ///< Floor on filter siz struct header { uint32_t magic = magic_value; uint32_t version = file_version; - uint32_t k_hashes = bloom::k_hashes; // qualified to disambiguate from the field name + uint32_t k_hash_count = k_hashes; uint32_t n_recv = 0; ///< Distinct receivers inserted (pre-rounding). uint32_t n_recv_action = 0; ///< Distinct (receiver, action) pairs inserted. uint32_t reserved = 0; @@ -111,13 +115,14 @@ class bloom_builder { const auto tmp = std::filesystem::path(path).concat(".tmp"); { - std::ofstream out(tmp, std::ios::binary | std::ios::trunc); - if (!out) throw std::runtime_error("bloom: cannot open " + tmp.string()); + // fc::cfile throws std::ios_base::failure on open/write error; the store_provider append path swallows that + // (bloom is advisory) and bare file-system errors surface at the rename. Scope the cfile so it closes (and + // flushes) before we rename, keeping the temp-then-rename atomicity guarantee intact. + fc::cfile out(tmp, fc::cfile::truncate_rw_mode); out.write(reinterpret_cast(&hdr), sizeof(hdr)); out.write(reinterpret_cast(recv_bits.data()), recv_bits.size()); out.write(reinterpret_cast(ra_bits.data()), ra_bits.size()); out.write(reinterpret_cast(&crc_v), sizeof(crc_v)); - if (!out) throw std::runtime_error("bloom: write failed for " + tmp.string()); } std::filesystem::rename(tmp, path); } @@ -142,65 +147,76 @@ class bloom_reader { bool valid() const noexcept { return _valid; } /// Invariant: on invalid reader, returns true so the caller treats the slice as "may contain, scan". - bool may_contain_receiver(chain::name r) const { + bool may_contain_receiver(chain::name r) const noexcept { if (!_valid) return true; return _recv.may_contain(r.to_uint64_t()); } - bool may_contain_recv_action(chain::name r, chain::name a) const { + bool may_contain_recv_action(chain::name r, chain::name a) const noexcept { if (!_valid) return true; return _recv_action.may_contain(bloom::pack_recv_action(r, a)); } private: void load(const std::filesystem::path& path) { - std::error_code ec; - const auto file_size = std::filesystem::file_size(path, ec); - if (ec || file_size < sizeof(bloom::header) + sizeof(uint32_t)) return; - - std::ifstream in(path, std::ios::binary); - if (!in) return; - - bloom::header hdr; - in.read(reinterpret_cast(&hdr), sizeof(hdr)); - if (!in) return; - if (hdr.magic != bloom::magic_value) return; - if (hdr.version != bloom::file_version) return; - if (hdr.k_hashes != bloom::k_hashes) return; - if (hdr.recv_capacity_bits % CHAR_BIT != 0) return; - if (hdr.recv_action_capacity_bits % CHAR_BIT != 0) return; - - const std::size_t recv_bytes = hdr.recv_capacity_bits / CHAR_BIT; - const std::size_t ra_bytes = hdr.recv_action_capacity_bits / CHAR_BIT; - const std::size_t expected_size = sizeof(bloom::header) + recv_bytes + ra_bytes + sizeof(uint32_t); - if (file_size != expected_size) return; - - std::vector recv_buf(recv_bytes); - std::vector ra_buf(ra_bytes); - in.read(reinterpret_cast(recv_buf.data()), recv_bytes); - in.read(reinterpret_cast(ra_buf.data()), ra_bytes); - uint32_t file_crc = 0; - in.read(reinterpret_cast(&file_crc), sizeof(file_crc)); - if (!in) return; - - boost::crc_32_type crc; - crc.process_bytes(&hdr, sizeof(hdr)); - crc.process_bytes(recv_buf.data(), recv_buf.size()); - crc.process_bytes(ra_buf.data(), ra_buf.size()); - if (crc.checksum() != file_crc) return; - - bloom::filter_t recv_f{hdr.recv_capacity_bits}; - bloom::filter_t ra_f{hdr.recv_action_capacity_bits}; - // filter::array() returns a boost::span over the filter's backing byte array. capacity() already matched the - // header's capacity on construction, so the span sizes match our buffers and the bitwise copy is well defined. - if (recv_f.array().size() != recv_buf.size()) return; - if (ra_f.array().size() != ra_buf.size()) return; - std::memcpy(recv_f.array().data(), recv_buf.data(), recv_buf.size()); - std::memcpy(ra_f.array().data(), ra_buf.data(), ra_buf.size()); - - _recv = std::move(recv_f); - _recv_action = std::move(ra_f); - _valid = true; + // Any failure here leaves _valid == false, which fails safe: the probe methods return true and the caller + // scans the slice. fc::cfile throws std::ios_base::failure on open/read errors; we catch broadly to keep + // that guarantee without propagating to the query path. + try { + std::error_code ec; + const auto file_size = std::filesystem::file_size(path, ec); + if (ec || file_size < sizeof(bloom::header) + sizeof(uint32_t)) return; + + fc::cfile in(path, fc::cfile::update_rw_mode); + + bloom::header hdr; + in.read(reinterpret_cast(&hdr), sizeof(hdr)); + if (hdr.magic != bloom::magic_value) return; + if (hdr.version != bloom::file_version) return; + if (hdr.k_hash_count != bloom::k_hashes) return; + if (hdr.recv_capacity_bits % CHAR_BIT != 0) return; + if (hdr.recv_action_capacity_bits % CHAR_BIT != 0) return; + // Defensive cap: a corrupted or crafted header with an absurd capacity would otherwise trigger a huge + // allocation below. Real bloom sizes are <1 MB even for busy-mainnet slices; 128 MB per filter is a wide + // safety margin. + if (hdr.recv_capacity_bits > bloom::max_capacity_bits) return; + if (hdr.recv_action_capacity_bits > bloom::max_capacity_bits) return; + + const std::size_t recv_bytes = hdr.recv_capacity_bits / CHAR_BIT; + const std::size_t ra_bytes = hdr.recv_action_capacity_bits / CHAR_BIT; + const std::size_t expected_size = sizeof(bloom::header) + recv_bytes + ra_bytes + sizeof(uint32_t); + if (file_size != expected_size) return; + + std::vector recv_buf(recv_bytes); + std::vector ra_buf(ra_bytes); + in.read(reinterpret_cast(recv_buf.data()), recv_bytes); + in.read(reinterpret_cast(ra_buf.data()), ra_bytes); + uint32_t file_crc = 0; + in.read(reinterpret_cast(&file_crc), sizeof(file_crc)); + + boost::crc_32_type crc; + crc.process_bytes(&hdr, sizeof(hdr)); + crc.process_bytes(recv_buf.data(), recv_buf.size()); + crc.process_bytes(ra_buf.data(), ra_buf.size()); + if (crc.checksum() != file_crc) return; + + // boost::bloom guarantees filter{f.capacity()}.capacity() == f.capacity() (see boost/bloom/detail/core.hpp + // :480), so reconstruction with the saved capacity produces an array of the saved byte size. The + // size-equality check below is a belt-and-suspenders guard against a future boost::bloom change. + bloom::filter_t recv_f{hdr.recv_capacity_bits}; + bloom::filter_t ra_f{hdr.recv_action_capacity_bits}; + if (recv_f.array().size() != recv_buf.size()) return; + if (ra_f.array().size() != ra_buf.size()) return; + std::memcpy(recv_f.array().data(), recv_buf.data(), recv_buf.size()); + std::memcpy(ra_f.array().data(), ra_buf.data(), ra_buf.size()); + + _recv = std::move(recv_f); + _recv_action = std::move(ra_f); + _valid = true; + } catch (const std::exception&) { + // File-system or read error: leave _valid == false so may_contain_* returns true -> scan fallback. + _valid = false; + } } bool _valid = false; diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp index a158b4c8ee..2ef44a490c 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp @@ -189,12 +189,12 @@ namespace sysio::trace_api { // Hoist filter state out of the hot loop: avoids re-loading the optional's discriminator and value on every // action comparison in the inner scan. - const bool has_receiver = query.receiver.has_value(); - const bool has_account = query.account.has_value(); - const bool has_action = query.action.has_value(); - const chain::name receiver_v = has_receiver ? *query.receiver : chain::name{}; - const chain::name account_v = has_account ? *query.account : chain::name{}; - const chain::name action_v = has_action ? *query.action : chain::name{}; + const bool has_receiver = query.receiver.has_value(); + const bool has_account = query.account.has_value(); + const bool has_action = query.action.has_value(); + const chain::name receiver_name = has_receiver ? *query.receiver : chain::name{}; + const chain::name account_name = has_account ? *query.account : chain::name{}; + const chain::name action_name = has_action ? *query.action : chain::name{}; // Reused across all transactions in all blocks: clear() keeps the vector's capacity so repeated scans of // trxs with similar action counts avoid per-trx allocations. @@ -207,7 +207,10 @@ namespace sysio::trace_api { // sidecar is missing or CRC-corrupt the bloom_reader is invalid and may_contain_* returns true, which // preserves the existing scan behaviour. const uint32_t stride = logfile_provider.slice_stride(); - const bool skip_eligible = has_receiver || (has_account && has_action); + // Both bloom probes below are gated on has_receiver (the receiver bloom is the only one we can hit with a + // single-filter probe, and the (receiver, action) composite still needs the receiver term). A query with + // only account and/or action set can't benefit from the bloom, so don't even open the sidecar. + const bool skip_eligible = has_receiver; std::optional current_slice; bool skip_current_slice = false; @@ -220,10 +223,10 @@ namespace sysio::trace_api { skip_current_slice = false; bloom_reader r = logfile_provider.get_bloom(slice); if (r.valid()) { - if (has_receiver && !r.may_contain_receiver(receiver_v)) { + if (!r.may_contain_receiver(receiver_name)) { skip_current_slice = true; - } else if (has_receiver && has_action - && !r.may_contain_recv_action(receiver_v, action_v)) { + } else if (has_action + && !r.may_contain_recv_action(receiver_name, action_name)) { skip_current_slice = true; } } @@ -253,9 +256,9 @@ namespace sysio::trace_api { // common case when scanning for a specific receiver/account/action across a wide block range. matches.clear(); for (const auto& a : trx.actions) { - if (has_receiver && a.receiver != receiver_v) continue; - if (has_account && a.account != account_v) continue; - if (has_action && a.action != action_v) continue; + if (has_receiver && a.receiver != receiver_name) continue; + if (has_account && a.account != account_name) continue; + if (has_action && a.action != action_name) continue; matches.push_back(&a); } if (matches.empty()) continue; diff --git a/plugins/trace_api_plugin/test/test_bloom_sidecar.cpp b/plugins/trace_api_plugin/test/test_bloom_sidecar.cpp index 71306cc3dd..4730c4d452 100644 --- a/plugins/trace_api_plugin/test/test_bloom_sidecar.cpp +++ b/plugins/trace_api_plugin/test/test_bloom_sidecar.cpp @@ -222,4 +222,45 @@ BOOST_AUTO_TEST_CASE(version_mismatch_rejected) { BOOST_CHECK(!r.valid()); } +/// Regression pin against boost::bloom capacity round-trip. boost::bloom's detail/core.hpp:480 documents the +/// invariant filter{f.capacity()}.capacity() == f.capacity(), which we rely on to reconstruct the filter from the +/// saved bit count. If a future boost upgrade quietly breaks this, bloom_reader::load would start rejecting every +/// sidecar on the array-size guard and every query would silently scan instead of skip. This test freezes the +/// invariant by inserting a known set, writing, reading back, and probing every inserted item for a hit. +BOOST_AUTO_TEST_CASE(filter_capacity_roundtrip_invariant) { + fc::temp_directory tempdir; + const auto path = tempdir.path() / "bloom_capacity.log"; + + // Range of item counts spanning the min_capacity floor (32) up into busy-slice territory (1000), so a rounding + // regression that only shows up at certain sizes has a chance to trigger. + for (std::size_t n : { std::size_t{1}, std::size_t{10}, std::size_t{50}, std::size_t{500}, std::size_t{1000} }) { + BOOST_TEST_INFO("n=" << n); + + bloom_builder b; + for (std::size_t i = 0; i < n; ++i) { + // Synthesize distinct names; name stores as uint64 so any distinct 64-bit values work. + chain::name receiver(0x1000'0000'0000'0000ull | static_cast(i)); + chain::name action (0x2000'0000'0000'0000ull | static_cast(i)); + action_trace_v0 a{}; + a.receiver = receiver; + a.account = receiver; + a.action = action; + b.add_action(a); + } + b.finalize_and_write(path); + + bloom_reader r(path); + BOOST_REQUIRE(r.valid()); + + for (std::size_t i = 0; i < n; ++i) { + chain::name receiver(0x1000'0000'0000'0000ull | static_cast(i)); + chain::name action (0x2000'0000'0000'0000ull | static_cast(i)); + BOOST_REQUIRE_MESSAGE(r.may_contain_receiver(receiver), + "receiver " << receiver.to_string() << " should probe as present"); + BOOST_REQUIRE_MESSAGE(r.may_contain_recv_action(receiver, action), + "(receiver, action) should probe as present"); + } + } +} + BOOST_AUTO_TEST_SUITE_END() From 83a2ffe99581b9220e61fb9022634efadfdc47b8 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 22 Apr 2026 17:37:07 -0500 Subject: [PATCH 27/32] trace_api: build receiver bloom at slice irreversibility (review item #1) Moves the per-slice bloom sidecar build out of the synchronous append path and into slice_directory::run_maintenance_tasks, alongside build_trx_id_index. The write is triggered by LIB crossing the slice rather than by a slice roll-over in append(). Why: under the earlier design a fork that crossed a slice boundary could overwrite an already-flushed bloom with an incomplete one built only from the replayed blocks. Walk-through: slice K gets flushed when the first block of slice K+1 appends; a fork rolls back into slice K; the next append detects the backward roll-over, flushes the partial slice K+1 builder, then flushes a near-empty slice K on the subsequent forward roll-over - overwriting the correct bloom. Queries for a receiver in slice K's pre-fork blocks would then get a bloom miss and silently drop the action from the response, defeating the fail-safe. LIB crossing is the natural guard: a fork cannot reach back across LIB, so the slice's data log is final by the time the bloom is built. Reading the data log in order still picks up any stale records left by earlier forks within the slice - but that's safe: bloom allows false positives (a forked-out receiver probes as present, the query scan visits the slice and finds no canonical match). False negatives are the only fatal mode and are eliminated by construction. Changes: - slice_directory::build_recv_bloom(slice_number, log): new method mirroring build_trx_id_index. Opens the uncompressed trace log, streams through each block_trace_v0 record, inserts every action into a bloom_builder, finalize_and_writes the sidecar. Skips if already built, if the slice has no uncompressed data (already compressed, or never written), if the trace file is empty (0 bytes), or if no record could be parsed (corrupted input). All I/O errors are captured via FC_LOG_AND_DROP; a failed build leaves no file and the query path falls back to scanning that slice. - slice_directory::_last_bloomed_slice: new tracker analogous to _last_indexed_slice. - slice_directory::run_maintenance_tasks: add a second process_irreversible_slice_range pass for bloom building, scheduled after trx_id-index build and before retention pruning / compression so the uncompressed .log is still available. - store_provider::append: remove the in-append rollover flush. Bloom building is no longer coupled to slice rollover. - store_provider: remove _current_bloom_slice and _current_bloom_builder members; bloom state now lives only on disk. Tests (3 new cases in slice_tests): - slice_dir_recv_bloom_build_on_lib: asserts the sidecar is absent immediately after append() and present after run_maintenance_tasks crosses LIB past the slice; verifies probes hit appended receivers and largely miss never-appended ones (<=1 false positive across 5 probes). - slice_dir_recv_bloom_fork_in_slice: appends a block, forks, replays with a different receiver. Verifies canonical receivers probe present, forked-out receiver also probes present (harmless false positive), and never-appended receivers largely miss. - slice_dir_recv_bloom_cross_slice_fork: the exact scenario that motivated this fix. Writes blocks spanning slice 0 and slice 1, forks the tail of slice 0 and head of slice 1, then advances LIB first past slice 0 only (slice 1 bloom absent) and then past slice 1 (both present). Asserts every canonical receiver in slice 0 probes as present - this would have failed under the earlier design. All 110 trace_api_plugin tests pass. nodeop links cleanly; plugin_test sweep green. Documentation updated to reflect the new build model. --- .../sysio/trace_api/store_provider.hpp | 18 +- .../trace_api_plugin/src/store_provider.cpp | 84 +++++-- .../trace_api_plugin/test/test_trace_file.cpp | 214 ++++++++++++++++++ plugins/trace_api_plugin/trace_api_plugin.md | 27 ++- 4 files changed, 310 insertions(+), 33 deletions(-) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp index 2de91a9f9f..d6ebc39d1f 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/store_provider.hpp @@ -268,6 +268,13 @@ namespace sysio::trace_api { */ void build_trx_id_index(uint32_t slice_number, const log_handler& log); + /** + * Build the per-slice receiver bloom sidecar from the slice's trace data log. Called on slices that are fully + * past LIB so the source data is final (no fork can reach back into an already-built sidecar). No-op if the + * sidecar already exists or the slice has no uncompressed trace data. + */ + void build_recv_bloom(uint32_t slice_number, const log_handler& log); + /** * Return {first, last} block numbers recorded across all index slice files, or nullopt * if no data exists. Used at startup to detect gaps between existing trace data and the @@ -347,6 +354,7 @@ namespace sysio::trace_api { const std::optional _minimum_uncompressed_irreversible_history_blocks; std::optional _last_compressed_slice; std::optional _last_indexed_slice; + std::optional _last_bloomed_slice; const size_t _compression_seek_point_stride; mutable std::mutex _maintenance_mtx; @@ -531,16 +539,6 @@ namespace sysio::trace_api { // ABI sidecar: one global append-only log in the slice directory. // abi_log serialises its own writes and allows concurrent lookups. abi_log _abi_log; - - // Per-slice bloom sidecar writer state. Extraction is single-threaded (chain signal thread), so the builder - // and slice-number tracker are plain members - no locking needed. The builder accumulates receivers and - // (receiver, action) pairs observed within the currently-open slice; append() detects a slice roll-over by - // comparing the incoming block's slice number against _current_bloom_slice, and on roll-over writes the - // completed slice's bloom to disk before resetting state for the new slice. If the node crashes before a - // roll-over fires, the in-flight slice's bloom is lost; request_handler treats a missing sidecar as - // "scan the slice" (fail-safe), which keeps query results correct at the cost of that one slice being scanned. - std::optional _current_bloom_slice; - bloom_builder _current_bloom_builder; }; } diff --git a/plugins/trace_api_plugin/src/store_provider.cpp b/plugins/trace_api_plugin/src/store_provider.cpp index 7145fac71d..8b6ad7215e 100644 --- a/plugins/trace_api_plugin/src/store_provider.cpp +++ b/plugins/trace_api_plugin/src/store_provider.cpp @@ -62,21 +62,6 @@ namespace sysio::trace_api { fc::cfile index; const uint32_t slice_number = _slice_directory.slice_number(bt.number); - // Detect slice roll-over and flush the completed slice's bloom sidecar before we start accumulating into the - // new slice's builder. A failure here is logged by the general except_handler path (finalize_and_write throws - // on I/O error); the sidecar is advisory so a missing one just means the query path falls back to scanning - // that slice. We swallow the write error here to avoid tying slice roll-over durability to bloom durability. - if (_current_bloom_slice && *_current_bloom_slice != slice_number) { - try { - _current_bloom_builder.finalize_and_write(_slice_directory.bloom_slice_path(*_current_bloom_slice)); - } catch (const std::exception& e) { - fc_wlog(_log, "trace_api: failed to write bloom sidecar for slice {}: {}", *_current_bloom_slice, e.what()); - } - _current_bloom_builder = bloom_builder{}; - } - _current_bloom_slice = slice_number; - _current_bloom_builder.add_block(bt); - _slice_directory.find_or_create_slice_pair(slice_number, open_state::write, trace, index); // storing as static_variant to allow adding other data types to the trace file in the future const uint64_t offset = append_store(data_log_entry { bt }, trace); @@ -596,6 +581,66 @@ namespace sysio::trace_api { " (" + std::to_string(writer.entry_count()) + " entries)"); } + void slice_directory::build_recv_bloom(uint32_t slice_number, const log_handler& log) { + const auto bloom_path = bloom_slice_path(slice_number); + if (std::filesystem::exists(bloom_path)) + return; // already built + + // Locate the slice's trace data log (trace_.log). run_maintenance_tasks orders bloom building before + // compression so a freshly-irreversible slice still has its uncompressed .log. If only a compressed .clog + // exists (e.g. upgrading a node that predates the bloom) or the file is missing, skip; the query path treats + // a missing sidecar as "scan this slice". Don't decompress-then-scan - compressed slices are aged and rarely + // queried. Look up the path without opening so we can check size before committing to an open. + fc::cfile trace; + const bool dont_open_file = false; + if (!find_trace_slice(slice_number, open_state::read, trace, dont_open_file)) { + log(std::string("trace_api: skipping receiver bloom for slice ") + std::to_string(slice_number) + + " (no uncompressed trace data; already compressed or never written)"); + return; + } + // Empty trace file => no actions to bloom. Production slices always have on-block traces so this only fires + // in tests that pre-create slice files; keeping it guards the maintenance path from writing a zero-entry + // sidecar that'd just clutter the directory. + const auto trace_path = trace.get_file_path(); + std::error_code ec; + const uint64_t trace_size = std::filesystem::file_size(trace_path, ec); + if (ec || trace_size == 0) return; + + log(std::string("Building receiver bloom for slice: ") + std::to_string(slice_number)); + + trace.open(fc::cfile::update_rw_mode); + + bloom_builder builder; + bool processed_any_block = false; + try { + // Stream through the data log record-by-record. Fork re-writes leave stale block_trace_v0 records in the + // file (the blk_offset sidecar only points to the canonical one), so the bloom will contain a superset of + // the canonical receivers. That's fine: bloom allows false positives; a receiver present only in a forked- + // out copy just probes as present and the query scan finds no canonical match for it. + while (trace.tellp() < trace_size) { + data_log_entry entry; + auto ds = trace.create_datastream(); + fc::raw::unpack(ds, entry); + std::visit([&builder](const auto& bt) { builder.add_block(bt); }, entry); + processed_any_block = true; + } + } FC_LOG_AND_DROP(); + + if (!processed_any_block) { + // No parseable records (corrupted or malformed data log). Don't write a default-sized sidecar - let the + // query path fall back to scanning this slice, which is the correct behavior for unreadable input. + return; + } + + try { + builder.finalize_and_write(bloom_path); + } FC_LOG_AND_DROP(); + + log(std::string("Built receiver bloom for slice: ") + std::to_string(slice_number) + + " (" + std::to_string(builder.receiver_count()) + " receivers, " + + std::to_string(builder.recv_action_count()) + " (receiver, action) pairs)"); + } + void slice_directory::set_lib(uint32_t lib) { { std::scoped_lock lock(_maintenance_mtx); @@ -759,6 +804,15 @@ namespace sysio::trace_api { } FC_LOG_AND_DROP(); }); + // Build receiver bloom sidecars on the same schedule as trx_id indexes - any slice fully past LIB has its data + // final, so forks can't corrupt the sidecar after it's written. Ordering before compression keeps the source + // .log available for the stream-scan. + process_irreversible_slice_range(lib, 0, _last_bloomed_slice, [this, &log](uint32_t slice_to_bloom){ + try { + build_recv_bloom(slice_to_bloom, log); + } FC_LOG_AND_DROP(); + }); + if (_minimum_irreversible_history_blocks) { process_irreversible_slice_range(lib, *_minimum_irreversible_history_blocks, _last_cleaned_up_slice, [this, &log](uint32_t slice_to_clean){ fc::cfile trace; diff --git a/plugins/trace_api_plugin/test/test_trace_file.cpp b/plugins/trace_api_plugin/test/test_trace_file.cpp index 075b788f69..bd47726b39 100644 --- a/plugins/trace_api_plugin/test/test_trace_file.cpp +++ b/plugins/trace_api_plugin/test/test_trace_file.cpp @@ -1255,4 +1255,218 @@ BOOST_AUTO_TEST_SUITE(slice_tests) BOOST_CHECK_EQUAL(*c, 1u); } + // Receiver bloom sidecar is built by run_maintenance_tasks at slice irreversibility, not during append. Before + // LIB crosses the slice, the sidecar must be absent (queries fall back to scan); once LIB advances past the slice, + // a maintenance pass produces a valid sidecar whose probes hit every receiver actually present in the slice and + // miss for receivers that were never appended. This exercises the full on-LIB build path including the data log + // stream-scan and the atomic sidecar write. + BOOST_FIXTURE_TEST_CASE(slice_dir_recv_bloom_build_on_lib, test_fixture) + { + fc::temp_directory tempdir; + const uint32_t width = 10; + // No compression, no deletion - keep the bloom build path focused. + test_store_provider sp(tempdir.path(), width); + + // Build two block_trace_v0s in slice 0 (block numbers 1 and 2), each with one transaction whose actions touch + // a distinct, known set of receivers. + auto make_bt = [](uint32_t num, chain::checksum256_type id, std::vector receivers) { + block_trace_v0 bt; + bt.id = id; + bt.number = num; + transaction_trace_v0 trx; + trx.id = id; + trx.block_num = num; + uint64_t seq = uint64_t{num} * 100; + for (auto r : receivers) { + action_trace_v0 a{}; + a.global_sequence = seq++; + a.receiver = r; + a.account = "sysio.token"_n; + a.action = "transfer"_n; + trx.actions.push_back(std::move(a)); + } + bt.transactions.push_back(std::move(trx)); + return bt; + }; + + auto id1 = "b000000000000000000000000000000000000000000000000000000000000001"_h; + auto id2 = "b000000000000000000000000000000000000000000000000000000000000002"_h; + sp.append(make_bt(1, id1, { "alice"_n, "bob"_n })); + sp.append(make_bt(2, id2, { "charlie"_n })); + + const auto bloom_path = sp._slice_directory.bloom_slice_path(0); + + // Before LIB, no sidecar should exist - the append path must not have built anything on the fly. + BOOST_CHECK(!std::filesystem::exists(bloom_path)); + + // Advance LIB so slice 0 (blocks 0..9) is past LIB. run_maintenance_tasks processes irreversible slices with + // min_irreversible=0, so a LIB inside slice 1 (block >= 10) makes slice 0 eligible. + sp._slice_directory.run_maintenance_tasks(/*lib=*/15, [](const std::string&){}); + + BOOST_REQUIRE(std::filesystem::exists(bloom_path)); + + bloom_reader r(bloom_path); + BOOST_REQUIRE(r.valid()); + BOOST_CHECK(r.may_contain_receiver("alice"_n)); + BOOST_CHECK(r.may_contain_receiver("bob"_n)); + BOOST_CHECK(r.may_contain_receiver("charlie"_n)); + BOOST_CHECK(r.may_contain_recv_action("alice"_n, "transfer"_n)); + BOOST_CHECK(r.may_contain_recv_action("charlie"_n, "transfer"_n)); + // A receiver that was never appended should probe as absent (allowing for the 1% FPR on the small-capacity + // filter - try several unrelated names and tolerate at most one spurious hit). + std::size_t false_positives = 0; + for (auto n : { "never1"_n, "never2"_n, "never3"_n, "never4"_n, "never5"_n }) { + if (r.may_contain_receiver(n)) ++false_positives; + } + BOOST_CHECK_LE(false_positives, 1u); + + // Re-running maintenance is idempotent: the bloom path still exists and the file wasn't clobbered. + sp._slice_directory.run_maintenance_tasks(/*lib=*/15, [](const std::string&){}); + BOOST_REQUIRE(std::filesystem::exists(bloom_path)); + } + + // Fork behavior inside a single slice. The extraction path re-applies forked blocks by calling append() again + // with a new block_trace_v0 at the same block number. The data log ends up with BOTH the forked-out trace and + // the canonical trace: the blk_offset sidecar points only to the canonical offset, but the pre-fork record is + // still physically present in the file. Because the bloom is built by streaming the entire data log (not by + // walking blk_offset), it naturally includes receivers from forked-out blocks too. That is safe: a bloom may + // have false positives (a probe "hits" for a receiver that isn't in the canonical chain) but must never have + // false negatives (a probe "misses" for a receiver that IS in the canonical chain). The test asserts both halves + // of the invariant - canonical receivers probe as present, AND a forked-out receiver also probes as present + // (harmless false positive), AND a never-appended receiver does not. + BOOST_FIXTURE_TEST_CASE(slice_dir_recv_bloom_fork_in_slice, test_fixture) + { + fc::temp_directory tempdir; + const uint32_t width = 10; + test_store_provider sp(tempdir.path(), width); + + auto make_bt = [](uint32_t num, chain::checksum256_type id, chain::name receiver) { + block_trace_v0 bt; + bt.id = id; + bt.number = num; + transaction_trace_v0 trx; + trx.id = id; + trx.block_num = num; + action_trace_v0 a{}; + a.global_sequence = uint64_t{num} * 100; + a.receiver = receiver; + a.account = "sysio.token"_n; + a.action = "transfer"_n; + trx.actions.push_back(std::move(a)); + bt.transactions.push_back(std::move(trx)); + return bt; + }; + + // Initial chain: block 1 with alice, block 2 with bob. + sp.append(make_bt(1, "b000000000000000000000000000000000000000000000000000000000000001"_h, "alice"_n)); + sp.append(make_bt(2, "b000000000000000000000000000000000000000000000000000000000000002"_h, "bob"_n)); + + // Fork: chain switches to a different branch. Block 2 gets replayed with a different trace containing eve. + // Controller fires accepted_block again with the new block_trace; store_provider::append writes the new trace + // to the data log (appending, not overwriting in place) and updates the blk_offset sidecar to point at the new + // offset. The stale "bob" record still occupies its original position in the trace file. + sp.append(make_bt(2, "b0000000000000000000000000000000000000000000000000000000000000b2"_h, "eve"_n)); + + // Advance LIB past slice 0 so maintenance builds the bloom. + sp._slice_directory.run_maintenance_tasks(/*lib=*/15, [](const std::string&){}); + const auto bloom_path = sp._slice_directory.bloom_slice_path(0); + BOOST_REQUIRE(std::filesystem::exists(bloom_path)); + bloom_reader r(bloom_path); + BOOST_REQUIRE(r.valid()); + + // Canonical receivers must probe as present - this is the correctness invariant. + BOOST_CHECK(r.may_contain_receiver("alice"_n)); + BOOST_CHECK(r.may_contain_receiver("eve"_n)); + // Forked-out receiver also probes as present because the stream-scan includes its stale record. This is a + // benign false positive; the query scan will then visit that slice and find no canonical match for "bob". + BOOST_CHECK(r.may_contain_receiver("bob"_n)); + // Sanity: a receiver that was never in any branch at any time should still miss (modulo FPR). + std::size_t false_positives = 0; + for (auto n : { "never1"_n, "never2"_n, "never3"_n, "never4"_n, "never5"_n }) { + if (r.may_contain_receiver(n)) ++false_positives; + } + BOOST_CHECK_LE(false_positives, 1u); + } + + // Fork that crosses a slice boundary. The scenario that motivated moving the bloom write to LIB (rather than + // doing it at slice roll-over during append): the tail of slice K is replayed after the head of slice K+1 is + // already in flight. Under the earlier roll-over-based design the back-and-forth would have overwritten slice + // K's bloom with an incomplete one built only from the replayed blocks. Under the LIB-based design the sidecar + // isn't written until the slice is fully irreversible, so forks can't reach back into an already-written sidecar. + BOOST_FIXTURE_TEST_CASE(slice_dir_recv_bloom_cross_slice_fork, test_fixture) + { + fc::temp_directory tempdir; + const uint32_t width = 10; + test_store_provider sp(tempdir.path(), width); + + auto make_bt = [](uint32_t num, chain::checksum256_type id, chain::name receiver) { + block_trace_v0 bt; + bt.id = id; + bt.number = num; + transaction_trace_v0 trx; + trx.id = id; + trx.block_num = num; + action_trace_v0 a{}; + a.global_sequence = uint64_t{num} * 100; + a.receiver = receiver; + a.account = "sysio.token"_n; + a.action = "transfer"_n; + trx.actions.push_back(std::move(a)); + bt.transactions.push_back(std::move(trx)); + return bt; + }; + + // Normal forward progress through slice 0: blocks 1..9 each with a distinct receiver. These will all end up + // in slice 0's bloom if LIB crosses cleanly. + for (uint32_t n = 1; n <= 9; ++n) { + chain::name r(0x4000'0000'0000'0000ull | n); // synthesize distinct names + chain::checksum256_type id; + std::memcpy(id.data(), &n, sizeof(n)); + sp.append(make_bt(n, id, r)); + } + // Block 10 lands in slice 1. + sp.append(make_bt(10, "b00000000000000000000000000000000000000000000000000000000000000a"_h, "frank"_n)); + + // Simulate a fork that replays the last block of slice 0 with a different trace, then replays slice 1's first + // block. This is exactly the cross-slice rollback pattern that broke the earlier design. + sp.append(make_bt(9, "b0000000000000000000000000000000000000000000000000000000000000f9"_h, "grace"_n)); + sp.append(make_bt(10, "b00000000000000000000000000000000000000000000000000000000000000b"_h, "harry"_n)); + + // Neither slice 0 nor slice 1 has been built yet: no LIB has crossed them. + BOOST_CHECK(!std::filesystem::exists(sp._slice_directory.bloom_slice_path(0))); + BOOST_CHECK(!std::filesystem::exists(sp._slice_directory.bloom_slice_path(1))); + + // Advance LIB past slice 0 but still within slice 1. Slice 0 should now be bloomed; slice 1 should still be + // absent (it's still in flight, potentially subject to further forks). + sp._slice_directory.run_maintenance_tasks(/*lib=*/12, [](const std::string&){}); + BOOST_REQUIRE(std::filesystem::exists(sp._slice_directory.bloom_slice_path(0))); + BOOST_CHECK (!std::filesystem::exists(sp._slice_directory.bloom_slice_path(1))); + + // Slice 0's bloom must contain every receiver that was ever recorded in it - canonical and forked-out alike. + // The key invariant: a query for "grace" (canonical tail of slice 0) MUST hit the bloom. Under the pre-fix + // design this was the receiver that could get lost. + bloom_reader r0(sp._slice_directory.bloom_slice_path(0)); + BOOST_REQUIRE(r0.valid()); + for (uint32_t n = 1; n <= 8; ++n) { + chain::name expected(0x4000'0000'0000'0000ull | n); + BOOST_TEST_INFO("canonical slice-0 receiver " << expected.to_string()); + BOOST_CHECK(r0.may_contain_receiver(expected)); + } + BOOST_CHECK(r0.may_contain_receiver("grace"_n)); // canonical post-fork tail of slice 0 + // Forked-out block 9 receiver (the 0x4000... name for n=9): also present because stream-scan includes it. + { + chain::name pre_fork_9(0x4000'0000'0000'0000ull | 9); + BOOST_CHECK(r0.may_contain_receiver(pre_fork_9)); + } + + // Advance LIB past slice 1 and rebuild. Slice 1 must now be bloomed and must contain harry (canonical) - bob + // frank was the forked-out block 10, which the stream-scan still finds. + sp._slice_directory.run_maintenance_tasks(/*lib=*/25, [](const std::string&){}); + BOOST_REQUIRE(std::filesystem::exists(sp._slice_directory.bloom_slice_path(1))); + bloom_reader r1(sp._slice_directory.bloom_slice_path(1)); + BOOST_REQUIRE(r1.valid()); + BOOST_CHECK(r1.may_contain_receiver("harry"_n)); // canonical slice-1 + BOOST_CHECK(r1.may_contain_receiver("frank"_n)); // forked-out slice-1 (harmless false positive) + } + BOOST_AUTO_TEST_SUITE_END() diff --git a/plugins/trace_api_plugin/trace_api_plugin.md b/plugins/trace_api_plugin/trace_api_plugin.md index d3c90be09d..de492fed90 100644 --- a/plugins/trace_api_plugin/trace_api_plugin.md +++ b/plugins/trace_api_plugin/trace_api_plugin.md @@ -79,7 +79,7 @@ default). | Option | Default | Description | |--------|---------|-------------| | `trace-dir` | `traces` | Directory for trace files. Relative paths are resolved from the node's data directory. | -| `trace-slice-stride` | `10000` | Number of blocks per slice file. Must be in `[1, 1000000]`. Larger values reduce file count but bloat the block-offset sidecar's per-slice pre-allocation (`stride * 8` bytes, sparse) and stress the per-slice trx_id hash index (rejected if it would need more than 2^28 buckets). Also bounds the worst-case scan cost of `get_actions` on a positive bloom probe - smaller strides mean less work per hit-slice at the cost of more sidecar files and more frequent slice roll-overs (see [Slice stride vs. query latency](#slice-stride-vs-query-latency)). Setting takes effect on nodeop restart; existing slices retain their old naming. | +| `trace-slice-stride` | `10000` | Number of blocks per slice file. Must be in `[1, 1000000]`. Larger values reduce file count but bloat the block-offset sidecar's per-slice pre-allocation (`stride * 8` bytes, sparse) and stress the per-slice trx_id hash index (rejected if it would need more than 2^28 buckets). Also bounds the worst-case scan cost of `get_actions` on a positive bloom probe - smaller strides mean less work per hit-slice at the cost of more sidecar files (see [Slice stride vs. query latency](#slice-stride-vs-query-latency)). Setting takes effect on nodeop restart; existing slices retain their old naming. | | `trace-minimum-irreversible-history-blocks` | `-1` | Blocks past LIB to retain before old slices can be auto-deleted. `-1` disables automatic deletion (keep forever). | | `trace-minimum-uncompressed-irreversible-history-blocks` | `-1` | Blocks past LIB to keep uncompressed. Slices older than this threshold are transparently compressed. `-1` disables automatic compression. | | `trace-max-block-range` | `1000` | Maximum number of blocks scanned by a single `get_actions` or `get_token_transfers` request. Must be in `[1, 10000]`. `block_num_end` is silently clamped to `block_num_start + trace-max-block-range - 1` when a request asks for more. The response envelope always reports the actual range scanned. | @@ -812,12 +812,23 @@ the slice (the composite filter scales with (receiver, action) pairs, typically 2-3x the receiver count). A busy mainnet slice sits around 10 KB; an empty slice produces a minimal always-miss file. -Build model: populated live during block extraction into two -`boost::unordered_flat_set` accumulators per open slice; -finalized and written (temp + rename) when `append()` detects a slice -roll-over. A node crash between roll-overs leaves the in-progress -slice without a sidecar; `get_actions` then scans that slice as if the -bloom were missing. +Build model: the bloom is built by `slice_directory::build_recv_bloom` +on the same schedule as the trx_id index - when the slice becomes +fully irreversible (its last block is below LIB), the maintenance pass +opens the slice's uncompressed data log, streams through each +`block_trace_v0` record in order, and inserts every action's receiver +(and `(receiver, action)` pair) into two +`boost::unordered_flat_set` accumulators. The filters are +then sized, populated, and written (temp + rename). Deferring the +write to irreversibility means forks cannot corrupt an already-written +sidecar: a fork cannot reach back across LIB, so the slice's data log +is final by the time the bloom is built. Fork re-writes leave stale +`block_trace_v0` records in the data log (the blk_offset sidecar +points only to the canonical offset); the stream-scan visits those +stale records too, so the bloom ends up as a superset of the canonical +receivers. That is safe - bloom allows false positives, and a +forked-out receiver probing as present just means the query scan +visits that slice and finds no canonical match. Query model: `get_actions` probes the bloom once per distinct slice in the queried block range. A negative probe is authoritative and the @@ -836,7 +847,7 @@ The bloom is per-slice, so on a **positive** probe the scanner still reads every block in the slice before returning. That cost is bounded by `trace-slice-stride`: larger strides mean more work per hit-slice, smaller strides mean finer skip granularity at the cost of more -sidecar files and more frequent slice roll-overs. +sidecar files. Rough per-slice scan cost on SSD with slices in the OS page cache (dominated by deserializing `block_trace_v0` records): From 9f314dd04292feaee6102b71e6994f9aef29d1a6 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Thu, 23 Apr 2026 13:17:42 -0500 Subject: [PATCH 28/32] trace_api: revert WIRE snapshot magic byteswap and reference regen The magic byteswap was extracted to PR #309 targeting feature/kv-secondary-primary-id. This branch goes back to master's reference snapshot files and the original 0x57495245 magic; reference files will be regenerated on top of #309 when it lands. Reverted: - libraries/chain/include/sysio/chain/snapshot.hpp - magic restored to 0x57495245 - unittests/snapshots/{blocks.log,snap_v1.bin.gz,snap_v1.bin.json.gz, snap_v1.json.gz} - reverted to master - unittests/test-data/consensus_blockchain/snapshot - reverted to master - tests/sysio_util_snapshot_info_test.py - head_block_id reverted; flush fix retained --- .../chain/include/sysio/chain/snapshot.hpp | 5 ++--- tests/sysio_util_snapshot_info_test.py | 2 +- unittests/snapshots/blocks.log | Bin 137428 -> 120342 bytes unittests/snapshots/snap_v1.bin.gz | Bin 51295 -> 47539 bytes unittests/snapshots/snap_v1.bin.json.gz | Bin 89317 -> 82190 bytes unittests/snapshots/snap_v1.json.gz | Bin 89001 -> 81904 bytes .../test-data/consensus_blockchain/snapshot | Bin 2149329 -> 2149012 bytes 7 files changed, 3 insertions(+), 4 deletions(-) diff --git a/libraries/chain/include/sysio/chain/snapshot.hpp b/libraries/chain/include/sysio/chain/snapshot.hpp index ff0fce581a..65fc2f9239 100644 --- a/libraries/chain/include/sysio/chain/snapshot.hpp +++ b/libraries/chain/include/sysio/chain/snapshot.hpp @@ -30,7 +30,7 @@ namespace sysio { namespace chain { * * File format v1: * [Header] (8 bytes) - * magic: uint32_t (0x45524957 "WIRE" — bytes 'W','I','R','E' on disk) + * magic: uint32_t (0x57495245 "WIRE") * version: uint32_t (1) * * [Section Data] @@ -158,8 +158,7 @@ namespace sysio { namespace chain { class snapshot_writer { public: - // Stored little-endian; bytes on disk are 'W','I','R','E' so a hex dump reads "WIRE". - static constexpr uint32_t magic_number = 0x45524957; + static constexpr uint32_t magic_number = 0x57495245; // WIRE in ASCII static constexpr uint32_t max_threads = 4; class section_writer { diff --git a/tests/sysio_util_snapshot_info_test.py b/tests/sysio_util_snapshot_info_test.py index ae589f74a4..bc2e3cf276 100755 --- a/tests/sysio_util_snapshot_info_test.py +++ b/tests/sysio_util_snapshot_info_test.py @@ -16,7 +16,7 @@ "result": { "version": 1, "chain_id": "144035215e20fd016e2b4b065349c959a1070fcbb0dc3f4784f3130685e774fc", - "head_block_id": "0000001d7105e950ff4a577b653dd4c75a805be942fd49f1da6f8e4642516925", + "head_block_id": "0000001de3dbb4005d41e5734afb622d192f160c98b1805314e4211900896249", "head_block_num": 29, "head_block_time": "2025-01-01T00:00:14.000" } diff --git a/unittests/snapshots/blocks.log b/unittests/snapshots/blocks.log index eb27644e88b98fd7f806422279a300702e3a58f0..972cb1ec6982f52bd6ba15cf7d1dc584b008d1b0 100644 GIT binary patch delta 46192 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 diff --git a/unittests/snapshots/snap_v1.bin.gz b/unittests/snapshots/snap_v1.bin.gz index 32c18f6e11237bf46cd4f99276c5c736125a166a..83c7195dc240cbf7e1f0a1d272fc05545806a791 100644 GIT binary patch literal 47539 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 My^jCg&JY9r2Vt9A9smFU literal 51295 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 diff --git a/unittests/snapshots/snap_v1.bin.json.gz b/unittests/snapshots/snap_v1.bin.json.gz index 35a4e1bfd1c910686a1927666e857ba0527fe5b4..24ce1e7803bf311d74f3f6a4c1157d48bcc03c0f 100644 GIT binary patch literal 82190 zcmeF)`BM{F+c5t3p1DV7Mul-i0oleCR0Kg}CwW`|9X1(c6G#LEL^fGNNJ#FvQBlG& z3dj;dMo>_;$i8G74GS7c= zzu5(cy^}AlyjiP{#e7~dTY)sdgHBVtEj{|Y?7W^682U@94m5ddejxiX z_|vFmWshLM|F2k6p(@bysRkiPL*#3ouWRz0T(=tqwC%e4xS+c*#2W=10lk?0@n-3M z!K>m)|L6OM2_IJzpGh74i|v9d4s1l+)OOFYpEx%RvU~e@=1-uWPE^=KTfD_-Mop#b z@0id~Flh9@M%VmrseBoYmC-i~w=urm!&ldeRuQ0#_C%mi>J;pMI^AD4#~a6`t@=)` zXcargZd2~K^NS}&dm=hSsA92h18{Sj;b+Jho z3^VqO+?f_0sJ-(%#bzxq9MRz2kerMV-#q8rZi(>B1rqGBZU;bMZ*L!fza^q7jBBI0 zT`IjHiJJ19-7^*-04`)IJFvBVf?_lFOjNo9K>yOf+EQOd;MSqN8l`K%Dc7-);zWRF zdz}IOXYFwu>Ugsf1DX^}=77TXD>2vo)7_)&X5D7a;nUgrT75*{yTFIxrY2IQE60ZN zim{1y{`I`2dvbWN)`s`9!xZ1&qH7V|zqTL0pI~oTn&XQrykZ9GfZ{B0tmy5p?N&@j zuVF3g%Iui_sT*Fuf65auqGij=cR-%PN3^!vhbM~N)UT}IgIiQ>%NF$5@vB@&x%J3n zOnAj>OlIA5e%q>T6vo|WrD((K>DkJ_YyW9_Q*+Gy-1V9Z0`H6WSbUR=;`kY_b#zEJ z;^@$cgo*4~of{5oO!n!wc}-2L_Y{~co2FH3oTS5SwGZuf`1RzwCy8j+n9+?sV`=65 z8)$31>B!LJ;ufLL$sytNNV*kz?+cr(@QZKhwM3WXF2<1+`Rgyh)z_Ljoh*9k6&=aD zmHg+v7oQ-eKIfPVI(^R?xk0bho>SgSaw&FI>rZFEYh3x(&!X%%$ZK_UDB~;SwGUIDk-YrSrD8m&uO}un7G!bVw2o zKs^6g1Z1l=db>5TSvz9Yz|fY2=HtX$;e%-fc4#sP$z11SY@w^AgimSoulB&%OZLnm82Ck1v!p)i-5fzRC z)HW46B^XmQOCeyOQS(BD@^g0%v0z3yG)CoWNFbo^H$qK~-r&LR`gVV-32qtnbF?$! zde{AGpZdN)QtSi1q+C-3i@x;5{OWvrbJ|M;Y=;H7Xfc%{3tLdOgiIXV_k6jX-Lagp zdUiX$f^YTTTV77zJ$>fjfsaN0^ldbmwb@Q!>K~~c`=xQAt>?=C8Sl4A70i%VDpsu9 z;DsInXk4t_55F7jS!tJ@&?YJIe0yCql>p^ku7GE9;J4@(M^R zb~HG3{PWCqPU6fE%<5k&{rx8e zHbk@h^aKqmU1pv%V-Es)bq0@w_=gywiE>5Y-2Pa8do|4x3#gXG5fuNVS@>Oet75&OK6j@ zr*GuXi>r2CMpwg?53M{O>Q)q0xZ3F-Uc7MoD((>S+LBt^^X<4^TwD3@(hVIS(-B(7 zumEKF+qGj_5tRnt{zeWA@LllzuJ+?iPv7TFPY<>;(*)kmT(bJpN&K4z8hU8$bKB0} zx7w4IX1EW_1a;eR>jVJXrr7K+6Fl3sKNvXi)XAB&j_k}9DN#<`FlGPigetUpAP#4V z65&3P=wf-G+p|yV1uvG)NSS79VJM0{VYSk$ycl@8C=nl;q^j7*NMcdGXwOnyFQNdz zbLv}^H^(­0K-ev$U4;;O@pc16cI8tr5qopP@=nDMkQ^ze;J`HBDBRfPw0^K|Y?z;8*|>=uhb%n}o_GB%HItjz)OFJ#-DXJ7=Bc>b!rFsxKpy1e{= z>=)0649tT>7o7UC+mnyTGH=BvWuLdT1tl7P4u-ZOyL;JA=d=+>qCG%)GhT!=Ys0+S z5hk>yI>zC8ogL?TojqfC(U3@qx^GDB?=a&9xoyY7H&}K#Qj^t_1LCda2bOOwWbRZJ zZ%91v&5!C!g~H#ZhD9mcTl2BAhVTy*X6KD zVVQtO5x?=4^JYdJ_uk+O$KH?TTC>P+xddQKms~WQBW8B(3@3*oGo3BEH`cc9IG^FH zmFB_%k)ef3=r&-O2mC})j}4U3-D5*QA%V$U)*F-=RW&M}$Kwppcyj695VkVw28@?= z&Zi+QE`@-92LY@dk;4Qa@U7bD?a5Z#2&aV5d^_8*g~hdrG6i4(E>3#_6v>DC36 zH2}~=ugsO06pruIaX6yEPF-K`XVIdDUm*adL?$=m+DR|-0hUQ8yc}i>?2=?|)M5zF zUejtHI>nvJQnO@j$oBX^Fc&NQRH#LFqoo)dMWKhzBDG^%9DUb|r!vp7!@CqO5pQTb zC{OF)ZDomtHThs~Tk521ioYvhxI@`Tn65Mu2}7?LVMyj)Z$vs_$I&YLZKI;mBhbuohK>!goa zvlzYyu^dJu5svO%0CgwF`__%^X`rYYmqqP7v)rgkY)x~N_9C~^2}0c)g0Y6-t@%t# zMp9|8k!nYXmu_$g<Bo(;!(=nb*{wiTmzt}|*o>1dvH3hZr3op?Jqa>JZw6{=lh?Tgd^|X@ z5w#PU{f;q}9a3gsP(QTYDz#>Z4B#^hZ_IQNT~$biH~AWxm8FN(&ZwP{Tg>(~Xmdv* zGyB;%DTIe`At#P8LFB?7Rd49z%3ag#`6+0tLHb5m$ZRa%GQ_<&IX_2)%tNrBl8}cN z*>4lE_hGyz6+`zb5OI^tBavebTI>c z2zYu)_MA&$TN1Xl_a1e2gq|>Chv(s}PM=@;cdiwF+69!LnJ0v+&68|v!;Dla+7Y-uxiX$Jkb25J_H%-9V2qSzhl2HsalYM2 zjf%N=%QM|bjjGqySc1VGHxVR%oaSGpNOrL-hwU>ReDm87J)q4^MZ?D1@N9kz@=hz?YylcWgSEydy?VM-)V)d<$^ zMkumi+g@lCwiX++n6@SJf?-|pGt=dX{@oZA`no2J6^racrN+qlvpl>AsM}5U94#(r zylcfQKOae{$)`1A<>Y2w0ec%EeD@5IuU2MV(uxP%c50eBX-OM#eM$rrr4%G*HeyWF zPkWVug{MJ1fQ-Ub?bIzzaCXEca6dbAQo{Y{B7S+RZToLzXq7jgMxyJQw&NQIQf#_R z0ORn)_i7W1Q?Mx9$33-);~v97zi2%Ve=hHXS|8QgNDI<-fwllDJ-M>f zI!0^oHN`aoLBA$etDUh^M#k951UrvfMpa>C5621hdYpOsO42RATZ(l{)J;NrsN)(} zC%5s`M_<#&?M2k2fMbcg5j&5M!VJA;?R7c%058VhfJ55H3jwp16e&NCWFR}^Sz(L( z`sLB~az~5G_d->Pl0VhL`2Jrv01klV#Pb`O+v=?^YqoXsCMIKDSadx9$ zC%r!ZT?*loTw-z#Ucrw{)C{j^j3V?gHnYhQAzfXMurkq!Z4#+1au;8r!+fm$om6#j6!@ zH9(;nFN1d7D~tWiSHjiEF|5X(?10BK884A6{DH4Mw$rIt-RD4S2EU8 zdfa4a5)R$$3DvZxI9HYN+L{4b9Gvgz*0dS4C7HRsdKGF+Qo?mSR`^P3!_^qEy(vhNyxn{cho)e2X%Nf3%DqaBQytZ(qrRX3h3AYS6!J1| zCx#^&LJs$>qodiw*b$GlNXJt=_GUzqun+K~V z#o!oGlAx=AeTl^@!j1D9>k7y#5oZ+gCtxIyWk4bXnE*MO8G?(k$Mbe<#F-h6;64$n z$x2mZ1Z$Fad{_czC~=0Jsr<<=Lw8Wsi8I+=Lqm9H!_0&8c#MK+4ZOpwSe#NxCeQ`S zQ;3Shf+flGpc-oBeDU)3G_+jutU#&wa6eJ}(>^UNP7*0z(3gPLEfNJ1FctwPg+GeQ3VfJAunx_+ zv+=3sj>DMJgZ4C+`&5xLvTfv^Amh(g zd5O*)U9PlD{dW3ZAnKVZ%fOYMXCtcl!~r;xD-x`;;j>rj?D3)Elfz=0h#Tw#JYjk&O50nRn~7=&S92S^ZyO<+ula z<-ryO)mA=uYDAj4@_aK_S6kK-n~1{q^i7_?w=dDw?xwMAOr?r6@i}R3za-FBbFHth z;#1@Vg%J>F*Ql(Qlt|rZdqEEm3^|J%H z6s38U&mg~e>25_tmO5Si;W`Jo8qW?LIer|X%U|clg&2WB?VfPjVq7&b^{n?_$$D$HN%j}X2L2aHP zaAoKBMWtPQ>s_PoGWTVE{KmxuF0FdBC8=Q%&y153@Mf0Sp5=wM?6Tx^XW?h7k~-Sd z1?`JI)xCMqb_vS)2Ni94MS+J+&XoeCw;8I&1qn{u+31$~OM*nB=A#xzDJZ0dC+`<= zA0(Z-Clm@3m7onsw-WdzTAA9dr0&puM27yKXG^j*_*6cfe+dbw2*di+@h5jq^%fV` zw8Ye!uOCD++Qn--2xV4hSXk(Y+X}oLpdtLqPP8~=gAL&G!QZ_RjPX<}D)sV?N0l=; z!H3xuQ=63l>+4eeu@r6HY$EbD5w)C~~qKA0r zzEwwS0NMZET+G3a288gGk-W|gV_d0x3y_dTn0${yiyJ<(Mi$3zr1t&l)C;M%TRa<^ zCb@C=QJZu79L55YG`0FAS_rZ$rU|lM9$s9O#?O4i%H)uKwbg#5&D{vP)Fu1de$Trb z&$U?5(tE&-oqL)Fdvc<-XT4R(laJuA!L&$q#unwhBq*DCgDiP)FDu|=k?eRSa8`6G zp^la;ZoT2-CPyz09S?V?uFh|%@dvrA^{3*5pW0{Et4`Bmg4RQI0=1n`@;a9NQPl&e zO_&e1rQQeoEBDA>iq9Qkb5)ndY9k+~Bm)vmLmR_4;#%P-#IrluL28SE5o!AVpA#Hx z`M!U|2s+pJ)Mk_30kPTo2;)g-ihJleS67!ccj-2gpFtplBg z20d<;G3(vNvR+WJcto?_U|ILr$>DO3X79mMw3$ySc#{3n8ORFqJ1&Y2S1-uy<;K@60$`*5e|p4 z>@$rnKB13L)M*(|Vi!Kt#avcv5&0&Vm0cyCsdR=NhqX)RMy4*>p|#iT7)rJxKe5#g zQ+CTUEn@%e#8CEm+ZPT+%~3V9teIBkGue#{q$s&Q6 z7;x>eiR-BJ#F$Ow*^LEp_Z2>G1*ra%)D zf(3%b8@Krbsdr|5Dqjt`r_UNqs5;gsTgPdrx?f@Gvx1Yg)|TUmY@;Q%Ij*73z~j;7 zNda(Qv_EoMIIz&F3K;&=Fo5EVERU(RIpgTX89J^}HJp8Z-^Sc63ZtXC{5&A1Jk;ge z3`S6lY_;K|5(?4NE>4f^t+?e{@tIPS_&5iCtBXrChq;oM7bkx)nTWddsrp-(e?7Jf zj30_1&I%7yIu7AoVvSRlY96nbXIig6m^f+tBhn-?W!8f4#pmeFo)iPI7{B3=XUQ z;{DK`Xr#@FlN{RjW&Xj1{1%o+A2IQu!?N9Cd#QE4G|bvitI^lItun)i>r*p!@`jx; zY1PJ8dZT-Nf{l2g>kqgtIen%$(HFRPDShVYLXm$cuA%Mzgrzc;oHyS1^@o;@ude?s z-+%*qU8`M>O{5?67?}TO=C*~~%!TuPi+^$rUgd-64Mu>tyZa*;w+^Tabtq*4xHJ>*W`&V{f;`-}{c`Ts~4^a@cpw3ypVtL8Jd3 zo}YKOyi}MgiiHFn*lw*ERi+1=cC|C=UxnD;8;>Zt+p2CJex;Hj!*0<7eq|U)VNv=n z-FZ~Qet(K{ANXNT8#=vh{@%@QZlzD|&;y;PS(I16`;UXg@mlUA}-AAa`U=4m1Knqaxi%jL;m zQ!DOBj|R@2@>}e-d{`PStK=2&vQ?Ir=kt`C@;$*75XM=Q`lo;Wk_EAmy=n1Iy3nx4 z_b%JF6f#a=Z7vv(wI2UJ&U@Wb1$Waq27{DNu1*mrOeXOj6Yy6aOc=R(+-Q6AqRq1s z0c>oqd77waweE1C@%TwD|tn(l>WJPY4M+#d&LiJRw4KscBslk zvY^_nSzh~{yyjcCu(U|R(_+_K6K~caH1h>7hhM7;a&O#9K5`1=ddqWm)${l_M_gw- zPIz9cdiwF8$~NP~EoyTcq0nsNu9v)*Wcc#kJ#w9c+3eqyrLLu}L-o%V5lYn{$Y%ac z$@eAxI3vPr7YE^ML#zJo#D~Mg%gAZ^iRChAM^T+_WQu9R)d1jh=DIKJm#536btCt5 zes?j5rrV;ASJ&ui6`$M)lGi><&$Gepjm__;+%qJO{rNQ6{>Jvuo1=?t$CcM(4{}G^ zJ9n6x8xLdtBCB!xoWE5|k&B-zzDUcE|M{O^e)|63-~7J2*?k1NyIp_)gh^XE^+F&FN$E{rf-u_oSYV zj?F(i3aGY`7Q&(ooKPR#u+*AoeG*g8mGu5=H)!n!t=*ut8?^o(2Cd!hw%gryyW8#; zYPhT6u7Le;sz$MxlB_Dyz@;0DvyGbXyA@O^TdG(@s`+;_ z;Oa9w5a_lV395!?0(%uyq#CE$awlz*HB`;E`}Wr}Ts+Qxl#ycX07LGi@U}8lYn%Qts>xMgARUzEt$`9a(Bv!`9l6up|J~bV zISFf%eo$SCsRKCex70#$ejT%HXz!GM$?Dix)@;OIB7&>4^EKq}MY`g2O}sE_3m*B} ze!Bbx*0y?p8H3`q-*MSD^^2;S8}h?cdwe3mDWrsv@?~RYrvBb*d2Qg5ATL+hd_+o- z7qz8p)U-9j__^GdM?2(SD`EB`9nLRE8DBJ)>z!HPKC!Ccvkn$CctI{AgX4`}ETY!O z)U?U@J88+YEPLQe65JkS6lS~*hPwovizzN5qHA-+1D_eo@#CULblH-tl_z&a%gxfX zQJ1R~0OOiA!XWjozK0uOV4r$-J9i}==($HyWA3JFiq(=+_?Um#n5I(h2LUvT=!ih* zk?ewXh&x2$%sjPVS*NH<NVltc#Vg<+hz6$EtYk zl*t0<7J++|Tcnh-m&2}0U9D|RO)QVR$0U)RLUjf*@;ulkE?D^Neryr_w22&GCFx;PJOMaZrOIF9inI- zK@F`AN`|PuJzU}BklNMs6%So?TKCw<73V!@?rBwl$X%(Bj{VXvxFb=%U*(``g6Lu` zx&5pt+x~xV^f6p(zW000UP!muhH85rA9yT`r0a?yF$7>ZvL{ir_5eBY!+aG+CXaaB zy;AWh$yC=s@_=prYI0I}XKE(?gFM>(>+B!%vs!2D25;DWhM7eY zeU5?g4n*9j&z{=$TA!$`_!Dz>{77lCBx?iCW5@;uN9HeopX@DQp3x34g&a#-a(8V- z7t9i26Rn@8OZx77TjFQ?hV^kyUSp zHaxTcPwj${xS#okd@pZ)<<8s>K*|HnW|k=-@k30#)EthSnt4n6a#2uqq#GljheaL4 zVHUB+)#eMtTyg!6ZadJxPgiCi3$73CmvjFR!qyaP?E~LpAJI zBCHM%8fHuUsWuv9%d+xTand?|Ta%$~&U9UIx`ogmL7nX=9=Tz6cR+e4Zg$g;2x7s5 zJyFcPbe8p@4c?n+i$=ad3{^#(IoaWpySEng6P7c7x!3*&A?;Vcq(mPk*H6<&o@;uRX0+dEyxop?~7*=*RO;Homo1T=4v|Em>H73;DVwpNHT#PKS#OP zJ6p;u=_u{oNq@_`31PZ86tkEp3Acl*dt+?@c6%3v4{kRd3G7x5RWe6n&5k*t?hbNk zyiFA)w$%WHWkz*Qn?HUDHDBcn+lK|dU4yawvYP9+^_QQc&(zm)Fh<-X$$+l2T=q_K|FBr3V2s%^zo44W!pXM{>n>>NGQ(f} zG~V0LeTD9DK-~N0h}TYhT#Rp-7&I@9dL!o#Bqp6&3JLBIH^0CRU{~y?R{n51X8D*h zznM#qaoyQTC?$ZFyMc;+U$cgEbhJ`}c2FoDq=S<6|c;Z>hbO z!|1e|psp)~Tn6#7U0rYpCnDHeaa6K-=R`@_yjfb6|KSZwcs&Fj2%)&(C%0?z<==z6 z`9al!KBt)jv6bV;*!2fe6{i-FMj|V7baa|iqN7#whf7g7w4`Z!AchTw2Ur3HgSNla zxdI<*2xjOUH2vjwg2BEq8{U@VRBLAT;c=V(>|52%2|lRNJ5h^k(ZVf1T$Pq`GX8>x z{0CLqudEW|msPJe?{}rTxZoOZYOaLhC@wiHGN+-kWKWbCWgag%{yNGG>navT-8<3P zUZNY-+Xk?oUJa#ooQVu(Sxn^|Bpb|rL=SZr|3tc(+rWnyFs+DJXX{e_u5y9N??;Xm z2wnU51dt~n^oY3nrIc$?d6Q2UEXFLxz9FC{WDk8}$`b6a8u&kY+3zd&8bcl&PJE(_ zNMjL_^3u3<%vkTM3CrNpV=;}0c&Ez06Hi{W=~1yPS1Q|)Ejn?;wsy>H;n_K<{b2K2 zZeO8SaDk2yhsDJ;YVL<%->IkL?mNU;$e1ngJjT1#38u#4yOx`a;@;MeqR zyP;6Uiw%u$b*?>kK*{W#6KUoBw1m?*dbt687&psuM=ltc`1N{6-6N@v&ndU5Z7S1o zc;o(B(%*k2W{15(*p;v!PzNpy5cAUGs<+=&={jL<*_nEw6P+Y~NQ%#PHMfTSz@&CL zcd(K}TdNsaF}F+ouQPHs61s=Tf&Q#LuQJlxn;a#+_p#^#{Ct`rgP_kD=yD1*QNAo; z&Z~;bX0%p*{$NA!5l!2snUu~h1`&bNe*s@v= zOb994{%hKDX)BNd>Y=W_o8A}o5?~p_x_#)TAu^#N3(C$UojNuk6)!34-X z0qtg47A0a_mg7n`8Gf)uHjn_2wm(L7f1hs3_cwBqmQL?2yy|r8h3QL`BPnF?9Lv84 zxpwh`OH%HLb3yDC5Ghf{j1l&G%{u6HN0AiS zW1geBdHQM?{(Es;fS=;Y{P)sROVqY__W6jJSu>yY)yuy!@!YSqnVYxueoK43_R1>m zSF*i@r6?uDgwd7HNic(8wuL99ivxegCfX|JtB+}ew6FgDHex$q;LnyWik{tID!~)~ z2gmEzZd**&84v@duI(sL&0L^DM5&(K`ExbMfR0c9cHs zAivl>0n7{`gvb*78$$e@jQ6YVi)Kz1UZpcKTStWoU;Vu01VmV_bq9M�k(?6juDE z9K){lcfEo*ah}GebsipHK3Sw2Nyx2rCh2wYy8FsR4w%xHh=JtGqOquinDMfbcUqwpDRGu*R%uKSU*N33y6 zSRGibvJ`@y+zMm=?%&3Dn#VC|y?(|xfEN|2ax4m`*$OM82oVNtvsKNy$Jl*u_j^Mk z@zPTrBM#0`&@-wq0UK0^cvpp7&`0=sBd z2UbG^DT2wn5Ok<9?ZP*O(iB15SQm@8;MjRcgY8#t_yT~NwNT@d;iyCW$?$yc56MZD zK>?7fDsu@@n=*a&6LNhv>#*QvWtoM5^-fE&aW*&IF!1T>Ch}goU_G&=9=iV~poDVE zHWmC_a4VT>2iZ9KKfP7*ORfl{)Ow`oYDk4r0XcRcFUj%)p$5%A?7E(sioeMRK&q5; zRePi2ua?DD^MUS*Gxhcm!r809@qGzb;%Qe9g(-*(#B;0_@d_)*_G>zYz~?4}6{x1d zhjae6cxbbn{T4;xO{g9 z^)BADa2mPu+D8)%#LP~VnAT1oK@No%&i|m$OL1NrYI9lcihQlJ6r3CevFrxqw)p`* zQ3T4ALwqwnE$i=xSwAhjb4ucqVo63KgYA}ZU@844?sQBU1o$dOpX0%Ons7I!gqG^G z=X=#}YaUnd({)jOkn3fFd)imP#HOokau_mCkemF8*0W@f2+DOxdXf4ObdN!P?U1B* z-=sT|^^?rG&}*JE${a`z&E^7MTU}rCn3eUaljrn6;9qzRF+cHAqu<}zIH&dK%N+A<)y@VSK#-#hg z4;*U}wBv<6r*)U86sSe&nW~Y1auHD9hibLm5rjJPlf(fGk)A44GVB${)VQ**k6%}F zfdrO~C7JSLoz)HnaTyVYv#mjID9IKj61y(T@cn@uaWF4KO8&NXlt*ibo2A&YLcBkEf+jleoq+N-wOSz>wl9@Yv6|9&`fW;2*g z{uJA3!2BamCts$@sD80zE)^$O7>xbc+urGjDI2JFkAu?EDMfZsGrgbU>fB8ldZK3$ zN0|P8CP(^e=VH2qJ{B>QDEljpuX@9s_Gqc@*RQX8N*u*hJygl#R!vKzGg+_H>eM!$ z*4dw2#jNg@3_c*U=t4(#gQ<}VrpClVNpa1(iA9|&L-vDk&Z$$BUVVc9pakmGGAwB= z0SF!MI<4;$TOf=?e^*}u_QQ7UIe4Tqhc{zVB>#s_g>6%L#EWX47@^|xvQ<=`s%3{K zHdZMz$y*Y!I#{)?!e<5o`|(+ZqHYu9&muNbS2V9J=D>ACmwk7{9flK{y`SbyY&9V0 zoKZZr^Aq+nL<=ky_VjTf_b7pj(t(?Wdz^%bg$R}9qFI4^gUQ_L4WO7IOF5Sd`CAp2 z$*>ofN$Zu2j!&?nkNSuy>JnE zhJ!`|IAvc~ppmw!Y=QXo{@IGJ?U3^Yau#*~CjXjO1gfs7)=b3~nlM5c3`onYC)P$0 zD7Sb;^^30*z&qZWrr@1|jq+^_4aT2-_yKR{9>3*gLZkn2mbxd8 zoZ>pQ6mt*YM>^ol7NazGlrXd91n%q0^2&Ly`RpaEy$}3wclJW)CEQpkt#i93Om|eU zCY*bhb*ULOkonp^)Au!W%iDmYhbByoA#kDV%+i^Jac9f**hd}fSYt@^V%n_^enj+3 zf()G`7~nudd4Pst-Pi)>5nn%~O7(8_Jo6Vuif_!)(&3=Ek z14?k3f2?67y53=5WC=lAU*tbvJ?_KHjrGKZ&xvkiXItr0)61=kMi#2WLgnVO>0`$q zzdj#FG*q8%@#_?P#}Z~uKG;g^bxYU%aWr8jiHR)}M7%sdGa|^Emq2LJe0&eWIX`Yk znoX!fbpT#S&IngF7aW7lzXo&iIN+}SY;!FioUw|yO?J>eQ8#+$wcZ4+0~H6&Vf0vn zIAt$S-+pQAVuT#HRHAtB05qxU2(-s(7xBf;|6IVOK5m;Ei3!~UJ#Y-8eFJvbL35g> zVt<`4B$nbguz7$fs>^)%wKYmgy_M#w`us-rtaFp#4?f0L`%2#{=Z|szjKx0pdoQ}D zDur!39qM$HBLUJ@hb zVBF*8dxcL|z83wy6w?~EFY0`C^2Y=)hi~?^G`mBX!2*$efXB!LHdO0P14jBAjf`NU(jS zT(e!b>z8BeCbMB1%loIKDWV7EQ(}gcl3|om!aT;yT7ADZRylieoi&=m4qDfKB|F#R z5lDp``$l0Has9bBh+f2>PeE6&M&SnmTLSg%VrG=260R>tFc6->O*#Wi%<<1xolP}q zUcHxeX&1i{8{S^UuU3&^FGn$Id;VKi#xc>MwAFAe@b&I05yf43>iu*!2yPaEXHE5< zmWPa(@x^>bhR>#1B3G0P8BR$sq~l&~CQ$qy%ZOXa)nQAr^GocjA!wO*62MOUDPFI> zBq21#fS6E-?WtH#u`Q4UTn{oGfA+tXx`2-V13A>@NPv6 zwCEk%?taE^s`F|g-?EDq(WkGR>6&}p7Pv?PTyreMZybf_#dK$f{+KGci+rs)yV)M= zvNblFydHBk`zRk_J29z5$ixmMl32&wwh9(zGMD&eEG7SsQU20J7;`Vp{&SmO2P|7^ zjxjx>UgR@mk`)G>w&7|{bLushu}Icn*u{dF>FPSLapPhtx^^MAl?@)|G19SLl&k*w z^aP+jYn@x9kMH#?9G*|))Y_YGRLr=Ghti{}2QRais8-=BLFXz#9-z4Z61n6voq7wj z7Ej<<v+cs_QRH4iN-`WX^bt% z`t3AD?={JX;mQILL0y5oF5+xRS74IKYcIKOEW=9~|CV@v)?spx{IPxxSGKYyNUGx9 zXXRBPhy<(yjo%`odFjQJ;4KlL#3Qj)x(IIe&o45gqaCK==eiHmJE?MQY$Q#SFt$zBeq^u)q`vGpua&=r5;M8>%dArQG zVE#AIn$`z+dEfWJW(B4%(f%C*N zIvCjmtmYVDT}%oaN^t2HLne~^6vPJO5MH7qQ0?e4+rF(>|Ewr$Ed5C~a?6-HNv(;c z|K1fYXcAR*hvPMzO`%ecbLWzR*sR?ch!z!5U{35zV~b5YX|!LRI7o@%vxZ{H?VVe2 zNxVR*EV5HLF?KLa*Bx)~oTPK-z;4u9TnIQ(o<+N;I%Z@S!12LxNLh$thE@G#|Iko= z$AOh`Fv*VX7>r`NnYiEsW^5TIx>YqD{2JFQAUzcBG^C_tK0y^2WP2nSBgpfhc) zrz%{l$b*-#H@=ScNn43{1EV9gD*>%`BNEYO)wnbhSZJ5~*5Z*|>#B=Zx^wuAwTu?dorBlU74d4)s?X*tDZ~vvUeR=|66I<)#kbEWNNw>Zd&fXIuam>CZh&Oa6HKNwT;IbOh`8?g^ zQNKjH0W)cWf0hw8_AO_>_Eqx5_-OtLv8|$79;1-l58#ry*_#N}vWinHr)VgM9hW}E zLDG6!R1~qC^nG^JaQix{loi7ccD|ZClik&pX#$p`gD2_p&IkNNJV!DBXTl+8ZmRk2 zBCd0m#)6DpNCOoeVL4=vN-9=vWkLcM$&GDExrkSDReTn%s#Jn+dFsauk>_&gbW*m>AA(xbw>w9Djw6 zp8?=601Eimcu;O)YDWkMSagWVUWMwx`qN9Ki5+#Z*^wQUQvpEf#>FtP^W^bs8DG-* zUeB<5%^9=FJ?YL)j-uQR|CU@45L-paM9a_75}i+Osu#bwI+HN;^QB2wSX1Z6znR(& zu*M7ChHi>hJ2HZsL&>r9Kz>Y=yQGRsf?boml!Bj2o4AM#BTTzusl5}wa5IuUz>&@80ZoE*EwE|-1lwpwycKH$0 zKzan|;k!+SV^FAH57~C}Un(k-1L*DAn(Qpz80w70eg=c^3cI;l@;u-`lEOp2_%-`X z+flBstjQ^`EUDdxFB@{U9C?Bu!l?&4k+sfM32G8dX1B2wFZ;g-DGz#jH(25JRn0qH zuv=d*y(-qZ0M=Gjo$g^@(%>(^MLsH3;b9^B&OKGz$bzL0__oh9r7If9$gAl*J+& z;fNz#l7>^PEJCJhC|vJGB8w`={qMf19JoEH;*+yAszz?1W=KRuidk}?o+e|*2Wh8~ zgRA%s%ai>0M{zRU*cN7$7Ay5LMcFR%YKIp_m&wF*HI!~C$y^!i@P_Dn3iqyKKzL* z+;P4OlAIGEfmH#?U zD=x)E*dda>yo?nCyxIrP+%O!TQn`o_PARb zDDZ`oA`jby-n=-w6=FCd3FI#%Tx`~TbktYQn@uuR&lK)B2XeSBO^vcSDt&ckBlu4r z_o&vsdu}u$drNMyF)P{V6>{*d^(??SfOB(%G96Q`89}z6cqP2k4sXp4dd2iF5An{6 zP(acF-W%M>8;S?j8V>a?pq%4PqxXhB`h0HqGQK6d!)&ACkFB-Ti=@?uJ7%z)WPK)J zJBJfi9tXtXC9+W09j9+$Ng`%BMdXnx=PjcDKL9~MzQ4Lif2;0?e|5VL6Y9a!a1VbL z?zleCt?S8Y3eQoeIgD#A(_(V4S@s9J&71Um@92kcj6N@))BR;Q?&5azQ76>>_`SGZ zd)K`0_QQO!eU5Taq;>kVukb1V{hxF;YdGzOeC&sGNnvQN+vjKEnXkRgW%+H_`QE12 z5RckFBPAxnD1zYYo@DyDVOX@2ricAwAbZX)G`L^ZL#G-;(Nej;qD>QIXe*Z;H|F z;qrOHv=01r9P8)SHj{a|KQc#v`zoc)Aa8*_u;$VU)KNhH>$Qe`}s`5 z*ZVLGXGbv%xvtgan`t%N-yN*42JGtkI%Md4)=GTrrF0{==7wuVXYAK(Ga*2ksNdWpJO)G{ zIiujEjX?wmNk{@IfMfT+-!*wepdqtrckk|V?VK{pLNWWe=Jl;LJj3~AaJ_FjC*zOU zRoEtsPdqdn_7n2CE8+tbSHqY!AR~~ElGj=6^7jZkf^sDHb$*`lzmj1$)-snQKdiA| z9USl78{_Ekn7v@1%5!>!bD)9dSsNb+e`_`*+drPen3StE9W&2I9#!Pt_66%{R>TR? zI~C{Qaa*rac)t5#9o}JXp7U=!1#6B;A^b5@RWEvs`xNJLnOPtExkx#eucGG8h6^*0 zaQsZgWEeY_$7`$@n_#lncPQIlq3sOsp%}Zhub5x?7?a1g(zZ^3MwoE(3=55Ip%u!7 zMIP$Icj_3QO7YK!wV$xehyIhQ+GY_xevfz!jhT5n|nkMf*hU2kH+ zL5UK|g*>F*5#tOo-mm(sQ|OU*K!1#mIX@eARP~wj>r#io6~Zs!{rIq+2*U*Cz^8o0 zNOGn@asyfTV>^sj2_J!EQQVuIWt960do$4or1x;o9>*#|tR3W!im>69>XH5F$+J4j z_;B24X>6LDBgx}zOXKX7$2csH@d2N|*{J)O|43s?$0;7K@wkF}uhpk~nmNW-!@h_T z7dxuk*|jAsfHCwP+A~|poOUIj^6TeY*p_qHql6=*+Ak&=$h?@(^8&wBTvhN}gSqkb zhH`Xa%;3GT;8n!7iT?gdWOWto=DQKEZ}xeom}dih2MXSl*!t&lk}GyvAH?QMdmHHR z2Ilh-eQO!Zjp-_8>>ie5fmTSYarxbwDn94cwn!81F>(y(o=-6qxlNyFTJ9g9d}Fft zn{=|KduY#Jh0lCn!56bm317=$M$umAQQoXidp5Na=h2inwl>?Wwcl2q4UM_@^H!@at;p6PuiE*>oN8b*A2!whP>~W+D9Cl3az6iVzJyPq&Z8^ zi_=tB;`(x93Ezds5aeLSa%dJ~IHr9?x@w1VUqEhVya%!6BVL3j@z@k2&EKBEj)L`# zP8{1SM0^4gGn7)CNfMsx6eNPK@YL-#<2W3_wBma~ua`J1&gZ5* zaDP-3Ga?vJ2|eR`B!_x-!7>v0rc?4)d$eYM;5eY3&Ge;?f3G9LJn%SXT~TaGPwz}b z@w|P!mToI3PvBg8xD+Pq@ahOVaNNZ?X_7pDhL{sui}nTL|L^o}+#6RF#s}hlOAdIF z;{mP1ihLjSGKt&Y5{?GUC&hGTYW@N=8jst2{QSbQfVsxwAFyxWxkGFl*ycK6INZid zj244MCUHlpV773qr$_NLAyE3Y$mFfI$mB47De{GwXfNl*ha7h``h;x}lKh!0O(|dF z4D?vDQXSG8mKeJ>S_Pi!u}+vw;XM>Lrdd}}95<%fwxD+r**sPhrv?32)0lM;jNeQR zt_$7fcGLb&b{v0RVf|v|Fb$N%N8$a9+Yl&>_fAls=%Q>*{r8FNHmVRN3i8!s+{xp~H|jm};w5Ow zY)9zPI4o^yDujjP!`MbV?lT^DL*vf4R;ijYZJkj(8SZuTS<)QP`3Bm<;GM`fVv5zL zHZgWjj3<+PBpiF71)aFi;ov=Qux&oMKC@B#DB zRtCqD*HCJn46JpsuXSNAQ)o$_bUz)ZL8Rc?mfi&lN^1HP^TM%+hGHRkd^^MwTnupF zi*MZF2bpz&TSDIk{t&=-RYu>%_00G?Fkdr{--L4*GmCMkgfqyrYxqn$#})DlTG#K_ zxUP{0+dG(k&=Z0R$zn1-5w20R3Eq-?3S%EYKZPf}5Pj>U)W3}IeP3WbuX#Od#y<<8 zkJ}5IhJ*8f@K!a(m)kVJoR6;R^Ef{5f!;^6C*YCN&#lks%a4m#lzhECae2;U*jL09rs#a#gay8w@YvL8w z1Y?D!81vc7D$F-4>t#?{Cz-lBy7BnCZIy%eLcC@EJpjeLy9!f%f*p-)Y~_8h?(~l185HCF1T0Tq)L+?&e?WhreqR#oOJQY?Q) zx5i=6z&+0n7j821*oMaPw$M~#e9m<*OBzT_2*C6dh{R3?R-gX&~oG<5OTWj23cpt23 zlDiJ-iq7d-L3>v&zF1;@b>2Qd$6u|L*GR{$k^W5N%JLe>@uM{7HL{J6>@T;JD?&Oi zv~Nqcy%&tPq`2-iV$;C%b@!6bveZqK{XNP-QH6}OM7f;iNBF)U?)613|B>CrFjo3H zQW|5`QMcc#AS5|eT}{@xeTVII&uAObugn8kuEgH}_B6f=^RB7K3{|mrc{?ISc~CPIVQd!ZMNV#=q4Zgwu)Zj=tF!B{eJULy z{`{WgGtv|4NuBK3yjPMH*$FMvaMyWe=(1NxqOEGe3CnS)knVLWE^!k!J%WBX-|6u<(>xGn?Pnk-zkjM1LCUF zJ|o+;&>+-bNl#o3h(`nzn;6D}RAkcy^YDDHy&=!yJ^c&)M}&FSc6jTBeAMCmsR`O= z4Cf}=MX%@V@R(n9iciM#gTHn7HPPPVKA~I{7xF8ur-tlRzUHuHaZjA#FWGJ#ZOA+s z!$Jl^S_bz3%|f57FE;-YziJZC`GQ~Jadawol059Lg{Rj>?pg|B$R({4)LGG{dTtPd zt87#FkQ<>$XT1t5pwGIPJkGbva~Du|>@IgAgW1IjI04~lVxu>$k`6ugEapEpDgSY! z0=7%KSnQo15pFZt^3#=O|7w`$)}Suo^tS0cRtC{z6HoRd=i}44k8eQxsS>11tsE{Y zexoz8v)TxK;34@KMu^49vOT_c&hO1J5$GMu4{hRakbe+!*V&$f`K?uqVq}|KY?(H+ z^#jTEZT6|a^XThSm1ss-0iZWMqV5lS=s}r}@7Kn!jb@Jbq@wjn)Vzg`4 zu5)g_y(QK&MCjHc&Jk zjE_tA^m#3SZn@mr_HX*WLcg$Q0*MU`iui*8#qGd*6R{=vh9uun4R|xzx zpxOw0k8=5<#`!B?t>-e!acA<_0Uoc@Iep?e#c{=Wgg^6$w}MC1{tl05f6pW8A9=(Z z@QC@};}Lb?5w-uzBmPr70_E}l%On1`;t__htp6{M_}_y^%%GkB$McByrE>8q6BXuu zTez-6iU$xIk<(8M=bm#coYePXd%D(JiDz}TbUQUy8%??MmNBd{qRmX7H&nYK&^I-X z4=cqWt_YvA*wc)=k1+~Eq-#q2>k;MGXWU+jUr4kXQ%>gu>wXBvr^(|-eD;CSKlNY! zZKQLcn;X5JP~I8l-ruRb-?ZNJX2N|(3~OD)79Re|HnFV&<1l>cFZOXn@d$Z4>i@cL z8QW7fMEjA9L7i(ifv&11;Y!AvZ8!8c+l-72Db6Fr957siSl=-`!AbK3?;P;~OZZZn zaFYoC0BAPp&^~!+JDx5uo;qmvS`m&=9>*-$Opm00V_gVtfq_YVVt1fe0uYE)DKPcu>l=IUEQ#*Z)|Ffi))S1E|S+?(fSN6 zX{kbgcnK3hu(fc$hFo_`8NZ>tnsPP;oBPE-W5+?eQ;vb4+H-=PnkM!&qwjD`lJ+_r zCr{sX_+}1g;)DOb$?+2%4=acIre!qQIv6XXH8`mWp>j+gsSQ-ru@D-}a~TUwq&}|H z9(u>MVq8p# z&lY_cn(eE`VbGIbAFc@|L@ej%evGs9i&_w+S_TcO$t2h%QoBd6FC11~+F{>3;mEUG zgf*$`faMee)!eeI0H{<2?J@V@b_mQvr zp8Gc8Y_%fAH78Bd&~CpWj+fL!lzDi&!CI^fj{h%WsruprEXhd@Glf@IF0;A3kV0nb z(|R5Wp81L~$GD%kZbP5&>PSC`%JLy$mE-(r@ukRl^flYWx?ExxheGii`?S@!f(uY! zMp7)FQI&lEm^jfkH4Zyc<=Bzo)|%z!uj4>gN9CBP-|Sy?vVTo0)r=W~-?etlxiB=} z9N*;la;zLaq2e`><6pj6j=7Pzr;#e@f^mWEf^%qnqzkr+QI>%%VRoX39#=v5T4Nb3Bn}LX4Tx8OKa%tR-_#F;m=LHISGo!gj!z zDa3r_c&JJqGo`rQsuVL-s_TJ#1UOoRE>vxE+V0gWz>eBy>Zz_j@Vb2LCC?zH_(l4* z;qhOCTxn-M*XKZ=%ZO#lHOZ8L$dstNlT3+Nm@F^OzmO?K&OMye$DlQBbl=6-+dP&> z#{9)nYF)9d@7(&OVUZDj}~d* zAF#~dF702%G8ewFyJbuz=r!Z%+5l@h2}Y1M(V5~}t=LfQ1Ff7zL9sT-`>+D zkXI5v!kAk39QvT{L56fXIpVvDo=^MIr+HwVz0^Oc4QY8@kbVsd;8;1WH|=Dm^~~8A z`=2n(2>bFgPMGAwwC*gg@l*U~kqteT=S@>-&QU~OnjtU!0<$dFT^;Ti))l^y7_(pJ z>Lb1w#~Ytuiy_|Fz3|`yQ?A0YVj0J+jk=hTd_s}Omo7H@;q+UC3%JFaTpU~4EaS%o zQY+607K)kU3<^I2-bQh&l%oeSR+o8`W_cFdw!O$}9OwQ=k>?*_PgW{oKS3P3t?i_) zeys8xZZhRf61F$yO2o1s(Ehlky$Ed=%xf#OU=S2Fo(2VP!20RdI^GbJ=#gnkT`{n2GI((|Ih2XJp6r*kb$p=i?0xuq4eNQOcpsp=)OP!4m5J?;_QMwT#*OTa z)a7>Mw!g#L>-aODWl{rsk7XgpkI3G5So_zsk6`^pvBd1hTfsec{4EpxhI4guTElU0 zuo{>*%_V2&S6u^NL9EkHJosNibCx;H>6d9vJvyU0dsy$kryndo{n$YN+h;nef17USIq^S?KM2q*{fSx%xnXecxifKi>Ik*;jhV%=N)nme4Ug3P1-^(?tyN z=MwWuIKK+|>(+Rjzax&dPq$N*D}#Ngi?haOU=caeE>CK&zV>^_OfO%;WgHu}+toi_Y{t zUlQ7Kr^J8oNd}{J;GN1}%08v0gzuU-3=CbWS5V{-u^r13y2*0g+S!^q(2s^@`#RZx zU_E@mam6|z`)zJNmKb_vZbq_(m9uMQCs=dXj~t`pLtS1kHdLFV8wYuAp z)usG*U+TezbMg<2I99vAJX!1HJK(w>7{4ufJuv@_;frz0des(OG@$uJAN#=>%Orum zV|$9WeNFW%^V%6%ZtpB(9k+VvCmR^ghm6UpY@c@5C=ZZyg+yw%Ca_K{@vg43Rb}>P zY6|<;8E|DgklfjS<$iEJ-R0MOV(s`u)36*^vPa_nI4Z}_H)VP8isePz6C3uS$Dz40 zzu_ZI^OyKYG)gsQYyD}DIoa>L^4SlN&xQIp7fhFuSUZdvTvrAD{zz({!nM8ci@cJ3 z&mVRJ)UUs=34gSGYo^nhYehIr!%ATB-6rX4ru$-#@))Z|>gzB+TT-m6@-AjM4t4!x z-LURJa)qj}jk$=kFWV;2e!$lbU+Dd6o7*^8-i@p3Ar+ag*=HZWqFO_@UY@_}8yIGh z!O-{rd)dq7+QY*CQtvI-K>Akq{almF>At^_>GYpux-aTeiA?u)r<_jR{cjrgr5pG7 zWZeIxXMk)R1ONIq|{r#pCTKgsixDLx~&CsNFZm$!>(7}}4duF^k0 z7D#JCm;3Co-cYn5w{^Pf_%6!X)xfe~t~st-){yHr#R@y0ZI|g;40JDMonu2jS**c^ zI8YR~O|mlCdvjg)p5$fDxA%adE|LfPRjLV0;gLYoN_LY5B>dTM~m~`FZ8yII* ztlfOiy6YVsA)TXqPqC%tdOiC}1TTT*P*{JwUK9I5v3dwMJklTb)~W3|*R|d<1Wl1# zav#=LDC-NcQqI`+JkV!ZK$s^QV-ql6#87+#=DmclAAYJmB(lA>}U64l?*%8&DC^qq%jKZ0%P)8 zhl|(u`_0eA<@3+M8IRT2yJ#GqD($KL`Rn=@-rqofFOBsL?%9xJ>o=t1V~pv1!QYJY zV3fJqmd+!_wD(HSOep3fw9^dp+Wph}(6;vVeOPAmvDC77@ zvoxwdAs;T{sli0=IoP%mg_1B(TkMVvSP15Hbj4@KSvDbK;{Hp)Fema}nJ>VD* zaZcVA$@8T@tX#hx-)fDTI#)98?U>iXNN}mfx`DE0SH@+9+njUqd(U?AX}BZiL1Bu&aQXWTK={k@&)m+KTteyq3lyI8;>%4(oCwsO3;-=C{sI;4X{3xi(L(vXA;&W(JLH>y5L%%EwV;3!tMNszuwdM zC^ka0%e-(bA>o>^U8rKlbL1EHqBf)7c6C>eWzM?w}5%Q zQR<9q5zPE9(=3c@Xef-&$NjnV5O3#__i#K~`I)+r`(nCp%lEBTc;8RCFT?w;8KcF! zru$N+cXs!xGO8m-u}y66iF4fa44lb6TTHOuu2?H9g6iXZt<^C+)h=$$t{+YweDYMFMI zyAO7Sir6W>x7i`?X3P0Gmfzdr&Qp(SmeKk&cAJ@%;`~x=JCe^#dy?o1hW6h~qXUnk{jN6#9zD@Cc7|SGM@*yV8#rF03 zp7@JNKgny$8mTSYm)f!?wYW~|W7*IC>!PM1*|oo`Q^vJ8rM5)brkb)$(9smP2j}fp z5e_fYwX~#V0=Y$A{*=@8-hM4=#EI?o5bK`ka_hGJScgvHfy;eC zZ0C)Oud^Iyt6J^cej;m)^;TMkjCrWo9+gR*%QA)!;!9CZ^4m}WZIre3$-kg;yBpSp z-GTf!+;J=#`xj2v@<7%Y#hyhRdp2@wOuIFPb5`1STo04$`zjX)=%ZdIJ|M(Kh3Wo< zz7dX*`JC4Jf2Iv~aGuv`U6Y^S&$NN<`AkptUMBWNvElEj4&2)xeN?+1(!+c` z9bBD5wd5FLnCGR$eUMzZe=#kqLua2{>XSOCxED;TE`AA3ezNzBX)*h8cIyxPKuL`0 zQuYx@Gu@r!c@%u0FMI&?f8qoERB+V%5+5)U3rTg9#TRA@d$qI&_Jenka?Idu{VRLD zapNsMi8}G9?l(N@`?i&N6xzo>_!%>)jl*;GZrM-iQ%!ZL1<3RaX6d_@Ed;I{u4j`O%40>&wH*%{P>^Ta|!cstb>^3F;HylTl!k|V*NM! z?(L8J?p|^Sx?-QjeYeVS868i`P#!$lgESYB`aBn%|HNDbf0_&L!(4dzTvWHe_iy*~ z?vsBz`K^CDo1$EIk^@0;$&_Ew$7@SJklF<$pS5A1^}z^_^+U>Gz*v$PbKL^OF}3@Y z`0X0zF;w>b@Kb)pGd7CELn2mJ zPR6!Ekl)rtPGg{3taH0qExbM|UynauFJXolhZ)j(Nnvq5eZU=MKi|aeZP=;S4d7(%goAmB-&=Zj+C|tsF1H_>ZGR@c*%1 zMC9s4Xu|pIyPTm&>P1l8iH7}S)YfO)m_?jHt^>icb0~EjsosX@&!Om%-;C9g?5>LS z&1zmBxX2ZyTo;Z@L0+`>JiC@q~>Tm0r{`;dhUoV%CWSb zht@kp7xUMow3-%nx#1=TZ`xNxV`EObbw3Ec2(_SgE$|A<7#EEik@&n z2Qkhgv+=ZRSWvtb$WMYGSEc^A?;S;Hsc-@ zzydTQzO!i@UF9`l1<;BXpyP8wQ`k;I^ z#O>NEmqRwgr%n%!y^?;Y?ErN?Q*|){2IVP=#o#>JDHp^Wd&bbxXZQ}%!I+zHKS zI{!1qtA=0azy&TJifvMu|3-218gsH3)1s2+WQ81`3p5{d{Y!NTKIHj_MV|kdbFx-W zehP}5tUP~Aa~pCjfPS9m&-NV4@+in_QN`6(_a$b;^8T|DGa~*}$v-yY&o{Ze1Lyzx z+F5>HJFVH5Yv=YPC)lo3E*mykPU#0*)ZrxWK~t_ZV_?E~a9tzQHkxDni`cd~&y8mP zA?InAV%vyTeHr7|jz7fs?Jiv`8tF1!Th1%@^4td(kJ^`laQah+V|Z226#d`0m;$alR`~Nv{(e*N_S_!j>`c7J8TVcn zn4s3hkp)0Mf_WG7>jRRLs6IR4(Mo(F>5+q1^6^D|#8)?5lX^R~i?g${V}mdXuxPuk zpw2Mjoa?1O0KLIyy}|xdebFeROKfpTkMO1L4%@uj7)P4e^Niw>y;6-K&fl-5e^XvS z>|Gxomb`U4sC=8Z4%a^At>d}x^45QD|7zYk@gCZ5K)Xj2n_8-;G2|Tf3Fs$P7Yog@ zLt4lVecZFb-^*i3>qQ>>{pURPnDf(J-)zRN(rm||8U~C%>*kT{UVf?0&QiI2JSFn8M`NaT z!*UF!NAxNv`I4G6PZTdrZO3@BkMZ+OJ%u@|SQp!y681zvqY~_GVFx1Hb~#QsDPp2i zj_vS2#?f+}Am3bB$zK;aXz5Q&HY;1Ap9c17hxKtoxm7CHZ4{43-_?47b&;`bCG$|k zw(~m@!IbZx+dRgImpCJg7MlX zn@J#cF2{>K%#GrjG-NZ4&!yHj+Ty5gh|9siSaXUu2>88S6%@8C+7m@z)$aPUUr2C2 zoSog;Qf#w5iEUvlu4%#@1F!L@UMxsA(H8{O9BM#U2AO*)sk78y(QcR$-o?XSO&vGP1_HJgrC^?rk~ z()$8qrS}_*l`7>53x0~Md*{nzno-D?1z#|ZuSmSbe<@$d@lmdm-{GUY=XO}4nRd|Ap}mYT#Z)WH!`eP5gKi2L z6!|siAE}G$p*YnA(;1@ysfxHwGTfW z@8bLzG%wW4T0Az}lAeF6x34j;(=etP4aiN?;mpEK} zszvE<)xQ`UAkDt(KeVSm`|lrv-ls9xos7Xx&>1C9G*chst+nyER?io?OZQqgJ;pCY zc?@kx%p1gi_Qm>4N->HqeuuHonlI*h%Y2k#D`9Ws`8pWea}L+*XmddS)5OV03G1XR z(^bqF=dCrr)QO#c-gaZFD6Vr)exDQUPqba|8eiM4mA&?-{!m=L+l!KqU`fc#OLd3X~1|#Qm>@6ZZ3J< z#P!p4Q}TJR|F`MrJFFv-E!SVlmYQsAs!@|zL&I|_w6cPE2}Cay&^%Sp%QnR^5zWM$ zJ5}fol;=-$6Z2P;I#~lZMwjzhmj&IlALmQsUihHvP#)?{EU_k^=f?lj+{!j57lBO6`Wdyu?~#t>emNG{ z=ekDDj+&tVSa2h9Jj#?~s@dnPx@(Vd5`vFBkFY~v+@y{Oj~hye6=$C*a^7RI5qdN7 zb-{7S$9>|Px|n}ms^h>hp)B|LPN(iluH!!|&z$75uY)?47d z_w0M)Q~Ms(fKgln#?daKg?~-Y(NFzC&%s*Y-Z!?p+b?W)d$QfBFKl=9PqsVP&OT$h z{z1>VAU)@1^IxXtpj|S~ZF1w^*yP4v*yP6F+vNVN=luE~({paJ-|0CQc`R39S240z zoNWr%vtr$WW9J;Mpk++(I_W%a?X!O+65K%yV2?h~#ya4qP9La^_RXfrPc(IiQax-+y6VKVIw#i?poYnYAkKHHRUQ zwohzx)ZaWNtyAtp_|ca@`XS~UeUzQ@d9tY{@?Jejd z`~4`CdgiU}<9In4?jOdUu1pVlISu#kqiRqYcXc)z&L3u(ewaSC-{-@N%J{a4_q|Q4 z_1)+(IF5UxN*uN~qub_kRO>vdp2wPrjp#-W81ftcF$JZ{;YEC+lzKjohQBi zq1{W)+kXGN-5Z^^kNtYPx2`7sXy3k%tI2UxY2Wv&?fX%!eVLMw(!;f%)Z2b~e(jIy?PFTM_SYAQzusT>;)~?Cu3Y!}7w!9X z?Yfsw_IGP_Ox1EPL(#u)Rrt@(55Ql@e zvC?a}7tVDK<6*Fi!=M7s>}FBN%_W{+fAL(e+n<-_$l(7xo)!F`#s3BPKb`)S>oY~r zpA^W0|3|yIYT^HU^;fRvYh(^fwMAsn{YQT$#rNp@2!n>P{R%6M!AZJ)i~o!07~QYx z0sn_`p7QAacX|kqeLB9Rp5Xfowh!66qH@(KKGvU;G}(iP%&; z)-;}T>MtI9_&xPZ{iS2-KfV7Nk9}${4Rwe5yT)ULYgK*fFC8!Ocuf7J@p#4KDIQ1I zK7H|Re`EZfH=seM{^R$&dukLOU*Y?`9nBmbw`hERecylj9=aUTI-pti+r~}bUBitO`&#uwouy4L-6p=RgEnQl zW^V_mW*X_rU35^(c$KW1-OGcBOQ};CXFDACBQu<;>h3w(-T=d^#IkRi^XV_WJF9`QE!9d#}ms z;B7E?JlMPQ=iBiVAskVWq!uN)kZ*e$BJ_)i-j68BVUFqHEHcjFF z?X)&WUV6lLt4N=Z9{10qgMJ;~uG3j{2{O?h_-nfY@=pc$^z%GS(|$1UXs$Ay%^ok` z$19EhvT3b-9N)H&^i5b70oFNz^%l+_jwFwjW^J&S9;AjM7=mJ^en1K8_z@+%mnM-|FpT2=~Fb&Z5C;8a~{| z;qKwi{-$lA$b(8h!h2y39)9B3%)(22*9FKVo7v+4+q{Lb*+CmX25dfjF^(G;!$+EP zcn1492l-}N9kDDD%^xPi`DPv^TaaDe;9Xm61Kv9t*3)S%hP4r#kA@lC-;ihWaiFyV zYXin{SpgaK5x+gZ#ddVy+~5dffimgy{1MtTw3z#O1#@_q);b{HUcxuW4b> zCh7F?68aI`&%=i*oX7r?+&e}&ENm`kRiJ$y&pqBh4=+JBYj37CV0gv#;+d#(F~jwI zOKn+=hx2C`lSdeLkk#7nvnN>7HT^z)?7;e+!Tf-1{rFH#R1w`jPjOaBx4V<>Q8O<~fWD^?Mv%!d|F9 zLHl6MckU<1gq!L6YJJt|`1Aeo>|O09x6|F-!!&ysT{Jq;emLoRHy5qJ?eg&Yc(sYD zDwwAGs~^jrS!cCMGPv!`-=A7Pw)3CB|EIIZT5tc@i1cLqzFw~He*9`o=9xW9Ha1&N z569=$+EH!XVuO9#r&b!xp`jh_V?lD;LZGPGCvzm&yL;?KkQtDPv?W+{*aEJ zuDa{~(!SK1$8;GUel@1uecNP}djGt0^d5)5yvOHiYy0G-qyBdGe6{@9`myR(N0XPx zYjqn}_x<593h!3kO4o*u^GVRQzbdmU{bKY~KkIco^=EgzXdX9z4I5j(d-Qd7(S3Ox z_10%sPr>_fdbrs9+FaehI=FwnP5XDr%76@;RzQZ`pfpW^YD((&s7JwjvsETKpzH|P9OP6^r8#jtTuq|28VH#x5xCBO5xjSE+pVu$pkBi!DQMsJYq*3rD1NT{Q%W;@g!h!Mx${*O(mUO`C;s&!J}2+tw%uN&`bo}b@V(p1{5c(6!nYgT z7kd8k^!cZYAMhLSxjOgniM}83_wm`eJad8182258ztq1=>0jRW(lhj(u=KY)AN=4p zUEuRPUfI<_>xw^y<>*K43)rIXx7FY{s}2rx)MBfHw|P3=f!cmQi`Iy0{4jkQ1*2gT z$ULrTIu55p*uuEYKtaKG>FW8zba)OdyYHUq%xVwFh8M6sfh9L9w^d{(aNW{%JE{#1 zu!Y~|!6lG}_QT|M3}O$op4zZ`oj+XOgIH7<4IkcSnTn)scplzofkV)H5_rdcK72Tg z2bUm5bU-IxQ=3e!{Xp-&ZNCF+xrgh(_RpVzO)D>@XM0F0Zfn54s+Yh<+CVb)&(q-p zuwhW^>3io-<6GE%k7!ETEl~VZD&z4R&guQ5;rRkYhb58|pZnJS*rp^^*ZjTR#>_J>xqKFMdhT7phJ{(~V?}0R& zPoFSko$s5}#tR~a+VFia9`LROkRlj|iwDqGd7M~Oc%MF85~;qev8eC!dn}RJ;F3fT zn2!aKWlTh*HqVbVK0uC8k!|yyO&ENOJUG)~$AFLBN z$Lq zeVP8j*3kG*bz&3hxC_5y4dVcMjgR9&#=I={6xzpAcZ>$U7=I_A?2W>GJd139geo<{ zJDD_2qfXq7Y~sb8bvN3_T~M^UYLzVD+9J+k-z*z&9Q%=t=S_w0Hu#OXT2>y+YUG8ZIa;XY_%7uws6 zS9q>6SF6!n?sNay|++|2EG?xmkC1jIaI+WIjJiZ8<`gq;qb)VA& zw1oY~Ubfwwra=GI=Lim~)x}f|a=RuphXTJ#?h1x3krYHP4my&$CXhe{9NC(}`n_^k zzaDP6oL%;wD1s41(awlF62`k0IM*|M$F+HqVG6DmcdzkX+9Te>X_y2t@cp&w_tz-= z7_Y%Gr2%4jP;`a$gI|)4!mc(cw@LS}awukkjXFE4{hC*w_jcszbknKRcR1xWhgVQ5 zf?M-*_$1gR;yS{1yn1J7pOmx?6P;!A**mA=8eCI5nJXYAzIT2Abv$kAG-=(os{2=$ z!j1hoySRC+DJk94rLe#Qy2)U@FBOJ$Q28slW`l~I!-~d`p`~JXXqVi(6q>M3>S_vm z9^J7BcXpxl8$;s2rcU6#HKxm>3!TQF-w6zO8t&+xv-bv!d1Nz!UXD40n&7f1jGW(( z`?(*Ton`GkJxeKUAKFFXtz!u{20<#^ztTO6U(~LTZSlfQs|xgAQka15o?R=mm5}E^ z*E6I?F3sqn<-(-hSjk$uVV8RFU4L(Az0lZT*;#B?ok}TDdBJS%-s^vOZ%X$@`s?>r z-FMr+8)yG7jdR*)<>SnykGRj%ALgcCjK@>G5_Vn5mWs({_ZvztrcxN`4%6H`2NO6q zC*R>R8u-q|k8M5e6n*}%W)gxRQUZ<2utIQcJK=Z!p!qM)`5N|$E5(8DpLynHc=7$1 zRK6ZlYpp98>G8UzvcdM4!nY3}*YsdyYR~dX%KFcTLlU?0W0q zJ$CEqZhCfh){yce4A;f5cL9aOGu-U16;k=_%LBa^-TN3SVv*kvTwgOh5L}-HNzG%3 z*uIqdN-_Bt`#`XrlHQKxTT(1fh;z^BTzx#Z@*cNK&vMFyw;s82Vd#o~p?BePu(!QV z{!XAR&*ZxIP$tMzqbW74g>VXaXb7g@uT--LtBD0B09~RO3KA>t7szLv;!{djYU{-l z$`8-lTQSd+QuQVe6T$tpW=Jsn8|0$^uKN=hzXB55;{Bk8w7d!Z7Kyo3<8pPwqc%Oqa&S5XSKBdW~yq=m*lShE58YzFWxNu$wf%W!FVsZPaE8TtNZ=+EKvE2b~fesyrw#zAe+-vqT$><_~* z8-`#u0*2Wz0&^#6WK{aS`I4t7^)P&7D`A)f1sWEmvBo#0y0vd*ATt{ z!;IF-L9&-2)3}|+%LMv%S){)krkh-fj_<>eBYUfFigD;@Z@rcEitu~3#FLDqM|GHH z_D^W0$!TW)6U}T1%?*&RQF^q4&bITtKybV9<^4hTN}1Fy@7q#|?Grh=T>3PhCxq|2 zUAFvUzCMJs@_b>KlsXOlk?**4E1@kJhK%CxM$z8!XMP=YrJ79;j^<-pRFS%YW%2s! zw$mZ<{b<{JDP_iG)!iJIbRmYaaOuA+TT>_@!-V46_kliXNw0VOC5IL$La5tJCv!+5 zOerz0)Si6Xl=Q8(&f)&}y}Fdbr6oL%LmR`B(8dBb1LzTkwi4XJ?PcQUIt&gO>uuJe@?EeFv~9W+ya?#FN1<$8)FF&3 zjRoVr+!LH=S70piv1;>LI-J5U%ll%a{d|8+A!xnPeCf4%^@>d8LWlO{HN211O|uGo0jT5 zOn1p|zgwbb|Ect);}>)FpBk_2TH1YD4xB@`oed^09neg-tsrwDu3S!=*RX4rwd47e!&>E3#RwkRLiIiz_E@N}{;+V8l%-hL*15V#8mA=)K+tG>U^WkI|=wO+29Xi2&xLr_d* zeE8m~;`I8`_;ILm)4}GHQf-{5ma zD))!sk{ld;2}Tf0=fH3>oK3Y6V*q5ibdJZRPzfqme}c-*`H_?;kTma2YW|1Epj09V zOAZ-v*-(xXDqqkhg`y|34e zKoTSDV7V=5E8U!?#uEhP|H1o3QfBu>nfYQp<>MhbWS3~G>Kr$qPkbtY<^QH#G9Q0f zAGvIy7`EF;ijpdQtNNS0lx?LP$HclSkJ)BlzHwxa96>aABwa#9Dybj)CmAFj3pH(yO@E(p~pY}ly zNnHA2TEHU{54WbvWdr(qS1yUtHGO3VKVYo)KrhknpcAXE89Ls8av~*Q;BSJix?dsW zs%PR6pXy2IMHI~|f_ioq?T)zR`C%(o~q%__?ZW@cU z4)hNCeD>am=3FzgjkcD8-e_;d}m*Qb(|%B!u7!kC+^$t4p!O3h-}0~6om{0M!9Oe<4$ zRaxmcMZMWeu^h-McIn{Gh6r=g4axRJX?gJi+A*c{rIjmNuhEvKQFjF1t88x{=XNAn zUs>yQ)Hye;7d&uf6Bbl{I%sl9g1$iLW9g<1_L15TeH&3543}OzPr39am1%_Ed8$CJ z86v+&zVSGt1Q=&m6#MZ&CkPkj5yW6@f;q#7c~Xurm@N^{n&5GIQjXN=jjk+MKZ>RF zPh5gA3>d-T8NNX! z+At-POP=;It+sSe!fn~7*k05f&}PnKgx{Lo!^+0e4+AQNg=uj^8*7(>yaw6iq!jQ5 zOQkUkG<`?wG0PFSu3(B&;%yd|w(nJ<=PmDHk0A*_mxJexnz6lijD8-5ZB%B4C9qB* zs+b0F2|9#?$J8^jiL}o0|Bjap`(7M^p4FGQgk#0YQ81q@N1<&sj#o%u1X>GIi-#g# zT(n8PDAKV>j;nZ7>JZl^wpZ3B%oXTuSBawXfO~9@-2o(+Hq6_nhvH^KEdx z@w`SIe3N?eH`+im@?qXRW{OK>lieW2XFis z&{W~w=9K<1q^hK82lhRU7a;#|`BLg17GRDDR*7xt1&x;SY*ad_pq&xH8C25pn4|<# zFYUXcd#up$)Q!~@j4eZhqs+FBV${`!V&CQcfY&1SF_y;HP9vn1b9{bFzR0<*!>{cr zD{y)-*^Cb=y0X`GE#tO;w!C3H4xPV=2?|!uAC#4nsLiv1?EQ<~OlTPJ6PTMSK6{(h z)MaY$o9Lf@#dDQ#i@tKr^EYi?d}qXbaE-?qtjP_-#RCs`K^b<&_kr&BTzbY% z38K8b7ii30sZ8H&qh2z@zt-blSWYS>_!)gj@%yWy>@4ckbK-qpkEM2Vk(K5a*30Z! z9a0Bliv6fFgs>(Xr>u!$&CxyW7}o&IV+8vwC_%{N5XxApT;OfkZXVRhchs_~F7(@X z^qbb3i~~O7O6Oi=AZtnKT!shgI9}(|*uG|@%fHijm3fL`o?=QoMfRqQbLqKc(X4z-CVS0L|-tEmybuly<>*GfwhBeNHzOxv|0YzX?}0<_(pQ57D>h_GZew4Rx?}Y?f$~Kzq`H`poji zSm_Dbgv1Y(+JXjEaDP$xTlRyt@saP?2FgdPnX_fw1oRne{P<|>HJ5WrgqR3ITBbnJbPvo&R5x{ zGOMXtDsgHlmjdlBb6G>7tO5N<(LQ%_u1_U=7q|75Uh2V#&WUA9pgoH{@}{;|xq923 zZ))mow~UxiD1n>(Yu^GrU)|t0cHZmjeA1?GKp!&y;`=_;Q=VVen?jgVMRN-6e4Je+ z^o}FRN&ZRO;auW9uF*GONo_jwOtOc~6G@rbCwxz+jB^A%$@k3fe zya2ipldnQzdHlSLdMVz{!*Zpx|L(iE|Cn!4{8Y=8Is=`ju!A2c=8$9jCWEsH#$nwC z`oab+;R_FyoG`!esWt(mS$Q`)vbD zK1WiD1ZYr-yanTG4B}Fh^f4)eFesNwm;etT-c#5VD6T9)Ubc3;4WY_9&r+VKxXykd zf)JDv$qLrfXhJy~E9Re%6vvhlH0v6D&}IIH-%AI?-%$1-+B^~6s+RsmeIMy*%6i#f z*8jujYbrZG;4)gG59*5eB`~5#N<_MQ6!96f4N<9foSz905ncG|WPVnk=Lg0s64)(I z=7-|Bd48x2uf&jC5@f&35BsH%CL=6*5Vv9fglMx;Ypv5ne9-G~gz$}?C#94|XjM!w zepR7<&2e+UOE`wh6W>@$dNY@vqUT)cDe{%@+%gm-Gz*$G;eBfhdGkv<6Xu|ZoeBH7 zMjg11F@L>~%UzYf8|3A#dY`|CmI&0tp?@ijH^zCd}{1P$Qe4Jg1exFsLpRpa| zgMKz3p0Q#ZiNL!fO<&`h_QaKeMpBIX8)O zOo$HO@0HjkOsnpBqIaj)USF{e{NAwaCnVj3%h$k~ zyx$6W3HKwHu3dv{0{igkasczG&$X& zO#NvM!FM@c0N+)C6%@2ZH@KjC+;zKdVeGhDryG#RF=qW0ORaBc#u(vErc!up$9@6n4{fp?$X3~1&!r&ksWDjZbe_SQ=>qP_L`9rVqX&}Cq{%=^ek zk!j{5UB1A4jL@Ft~sy`yn7CalBWS(06YY(IzG?gKPEeO5on zPmu9FaJ{47LLC3D^WiLYRq2v#F@k887I_!k8*&*HvM17#2nVGrZt-R6M8S`=N+`EF7+?UnSf_5Fg$$i?nj#E)->Kfa`$g&j;Vj#FE91# zn8%X$m-=OA_&t;j-f%6X?tVD7KECIGF_Uj;>Dn`#LwW0Js}DFQyq<8}KOC>|7>H&> zIlENK zh2(7SgPbkL=98SQDutY_vYoCrt23 zC1T&zqtEtT(4X1PXV~wIeahfnb=E_X_fh+n5)%@hpQ}^-k^2H`Dn%csz;cVOkN<4I zdV#&8@^G%d7~l8lUeGCf!UK-^chqhRFZto@w8AUnzR;HHE>XF^k@-B)rbRdQa$l>& z8qq(ab6#?;a^EUigIk~OO%R%pzq4su_np>Q{YBrh=ll|X)z?hAm7m|g65WbQFE^d< zlx*Lf40YUcJJ1F{T9cf^?Eopo0{c%fmOiv2vYV>hAF>V;qLF)S-?)%}dq#O&CXjsu zc^~)~?{_f>xF$8lAdfaA-=|bwE2WZm-Ta#&^DK}%Ij+_gZ^r6|J(M zC~d!h@3?rUiqJ=tneV7w+KnT~)Z~LA-r?ri0eyHPat$BT`$50-Tn>lur9Rqz^@wEs zh0IYsX_C%VBRjj*)!fJH?Ch}4MtjO7Iz!pB@Le`AMm4t;-)t1Mf36$ETju&F{F*Rh z+A^BPB9BF4M~SjVtoAp)HyGZFQWV*96n$ z`~PW~HcpqKQ(*gFL#B-)hw**1#l4|3IveBwMVvD}M$kmS$^b0T<8j|v|(`+SwUXP-UiP4w+_NxsdPWn?%% zK{>AM!=9lmqFI)7e(5kn*!}6Ae995&e0X2Byp?|V2Yce*iu1ASk{@&vQ&N3xAEB9At1nmv&in5w&piatu>3ck1 z2@@?cKeAnr2_m3%hJDrTy7;P5en87`gf^+~uHk!u2B5rS=-yd|*W&UuXHPj&+vI)s zxJ^%w_v8zYjIM6nxa4*f&nvYNX+HnbH<6aZjEKB6L)(jyTs+kM*SU^t)6#rInaszg zEbo4t4=3-I#+%lHlWQfOz?S9R3ZMTV@50(?u)Iri!(&MOD8~M#&hqPA-&1S@JoAI) z-LL24X5;Q>nv@p%A5Cw67mXO63+IC5AMw{=Vb(qHOsS`&?~s5#kuZc?#H-BDbm zX}Z_~jxn-xqFr`}AW&>WPMnO%B~a%W`^Yf}b%{Z^W`Bj0|E1h3XPdM+&+is|KFMVQ zvQ@~v6#HloscBu>)0l?($C!o_f46b+yz@tUVlQ-ZGnPdx*_|aHoNec2zxl?!7v&IV zf5sTLZeHSZqxyvn&*5KqcyEEBAwKh^-{xFi1ME?9f0t?%+y1xhvXk{s;r%nm`)en> zKM}rPIpO=2g6CV}`FD=zE5ERlhM9hJ`bf_6P0oX>=x@$r9#Ym>m^Tb%?%kZ@FWNlp z?6nEQa?buP%1QC4VXg*VpJ>8Rg5pTe|m%I8*Ow>Dn`#6TO()#P1`$`1E*<$DkL7)Rz(I#no_4>oO>{ zaiiWzf7tLh@wXJ?1p0gfZRniO=d%sHqql0zHr_Ndu$O#^v!ob{-^zYJPGzzBsq8m$ zvR_?fzv?%#U%sv`)yew~C$ilQ`lus{(MDQ1OIc2wp^YZoVw(RPzGq;ABOQNJ!Hvjp zPlq&@;e_4+kATJL>}mJgt&7nX+amETs|Hf8Yx3SIWVJraXCMb`R3LIq6d#;_*2Rri zT-HAn>o_GZMx4SO(v5B_R zCi_fC_SAjGhobrOyQOQMiWjT+ieuJ*{@+$sFu!p8&f2-@}0)6@qm(%Z&@b|fJOd5wPwi6!HbwJn(AipVbt31CM2_KDb&gZW zW37<4%k>e;ITYhW{FiWESSGHgp~x>BceRr97kJ*f^CO*ZoqW5bbxryY#?8#x4u^UE zEI%PFXfAp)|0S1!S`AHmfox$YV~z@Wr5xYGWj!qg zn!xd)&-&+7Y^N2Vja8PD&d?_azn92o;P(gceu*sx*#z4VC#=t8?9;?jN?~KdzES-S zG?-%fNLL`A5AVrbZ-Vu`#?o8?m8K_oUiFil-;>I9ywAxqw121_tT#|>)&=SdGif&*|5PEtusn9<$Ep?Avz3daGyJZ@)%} z6RX@VwP71I$%FX2LH%WFoQ=Jfh%AIw*}b|#dO>Ipx$H2ccRSgkC?#1{jFCb1&}h&4 zJGP&5tUQX*6?u5!`T?Rdl~cxVkE6DHTlgTw*N{FB?HoyaZDD_@;ISesC%P|>Irb<{ zmr55>epN_+FZB=heE`qI*iN<|d9W7(j8$;)YCqNBg<=s%cEI(pjKo*)PV^k!f3C!5gfaE(7ueE%FJ0sLDPP;Td)Gt@niy;T0>Zp5 z?s4ag2AR~bFE;-!#@z?}Yq`ArYwA1MAAJ&Am6R}UIDf<819Suj@RDN91@nZka%jzL z)E4NWi_3;_r}ZItgNVmN_h89V_91w-*ExBQluBhE62`rtU$~fm&)JT7u0R{@me%_( z9s}adFkh{X^262X7*=GhY>5_PY$$L6!fa^)ohkZv!G7$be+OEN;lJ81_|o_re!+`= z9*s4rXQJ?KDD6@5AF7pmd3GF9~CGFvabCKw`z_pP*1m}$^W95*I zG;Y9NYOr1eJBed7jFjOGq-0_R`y=JkZA_S4BtIh`STGJm8TSsqo0j5b1&1Log~~a_ z6GXb~$~}UiK(U5>haV*vQ$S-+a49_IbCj>8-n7Cpi&-9{i@1=lghS03W#uxY4hxsD zFDTCg%R-MN$GL+Tk`X;`sr`{FPsvyZDS8`Q63=UT4kxm@C7dy6v%^CpyYz=P``l*1 zJnX929dnMA!%plI-h=L_`_r_x()CVXXufM2a+>g;aKv)}qL{p@f1F>n5!A2Tlem=EN~^uF|C zrXT&7&BBkFk{|O4WT+2*%-DU`>yRIlZK*4cfzchmN8fS2&AK7%&5u}=*tc@5XT{3g zuNY?pZU1T|b4f9fN^(2eI8zy zyGrb^AMLy9@7Q-=(Zp$LDP4?NgrD-oKH(=?ksbVmD@cKG0@Y=Df`@pp`PF z8prczHTZ{ibR#)ir?kJ%yaLXbdBtC}OVhlU=7e&($ODRFNV59(Nzuxv+rZx z8RH}Nz8;6u{5`XBwr%!#q+Y^#44UPUHjYWrPx5~-kHhiWOMcy{92tll+NNKQ7ZW<` zZ0EJvm-*Y|FVr4ZHl+3SInFuFe#h zT=z3&tmSo>5ieMW8~6oPmF=?GtRnXQF|4J4${BBIO^x*n`bgO(aLMv5;tU6Q?u@EP z4$fVxUD$4+z3o)H_{Qa|F7F??HdH}3(0mMQA9b9)7b@3RDtS*f${)w{#E%_u8|UA) z5s%aVue^8dN+j9Vg}<+#QD1wVUCB+M$2cD%G6P5yG(k{!#~C#sfdoOYL`7Vq|NA>{ z5t$%bR(1EDdQEL=F~*|J34nP(|9~Q&vy1uHNY8{x zoyG^Af2)!LoH^>8M{V;eW1s1&do|g&s!T`TMcXHuZ4J%mj^4YU|7qj$=ib$Wr8sGg@*iiZO;N^u8qeK#eEoNO{fOhicwL$1tB<}I zlbuH#dp>{JYwTz4{6+VFrMNTRzpe4lh;yW#6XVwP9AhuhUhE}v{tEbfGSH9o?k3yj zFZTR}b_&=clFtb9W5_mQ>h1u~Uv3>;uh~{Uc#ozt{N8sUPZ6D;*miHC?Jjne;NO~r zpJDA##*|3!???W@{n!T3FH|c;33=AO*ra|S&GEfRR?#z_xu5{ToU%5&v<^JiU|itdx(PrYv~qo zzGxre+QqqGWh3v8tzD!wVJMbNm?W;gvD*?`I*2+d%0(u{bt{Lk_YcyzKGw{GeUjk^ zFmLQyCHxLlcL=ta_!<@QEg;55oiT~WpmS$x#JWq?hU7WgHCRBMhu0g%QoF%To2?M8NOwB_)M$PM#c_Cnp z6}c7_IjUks`7C%X`Df>veVGc@gWw*JO^me(Q9dzT$IfHZ*Dhb7nPlIQr~QI_3sy#8 zyr>Q`p!LCHGQvA^k#SrNY*UXjHg!z3kL*8+Z9Ay%bWZ59Z}uIWjqu31L_7PtdtiT4 z8`E>i1Fw5+&mrFr{=Ub*yR`-Rg4=Q~F;sW0GuSeUpUF8NR#tL6hrG|tEboOuA7WlD zvHkT}ag4yfw7w(WugI29?Q@NsEZ?pDp8fn(KF`|i8CeU`{g?KMdS;)f`<|Q$cW&M^ zVpZ_DL~um9)_;k)%l8@m&v*%$F$^fDgZxCWw||eSc+WlgT21u2W&Rk#S?e-xi|{+8 zd-g(bk9Z9B{B5$Q!nv^(i&d!E78bN0mu z5gg#|+5G81aQDg)qrGF@HRvqI)fQE_LAg`6pdH5cD#W@4a&*hk0Z9}mSu=%eu z@w+wE$#YFq_B+l4i!$FO_F5Si>VK3l=Taj0Dg0Sf72OwQ-kb2cf}i(;kF3xl`K>N@LzA5=u$lp)- za;e^0Gd_~w0byPGUVV9R9tA}uK8aj;D?SOh|JA5F#Gw4DboO~CyuaAIxP<)^Evl_x zS;RcM7mG9W=wj8FZ+cC>L#k;v;Z`%QRMp}gg~*TSuu>wDk?jE{4`KRHZE+UYWoPP) zRRzy_RGH7ikoDk#tl>Z0w5gy>+IKO{ZpBUUDUAA;ruCis!cmDn9u2F+8&9&Ez))mfzwNoubcoo8l(Y`>;R7#BlrDp?ciMZajR(3FDPbU(p8? z%Kml{p-=LJ|JE6wq?MVu3D{=|-@`jEL;gV>9v^vTfSWKU}2aP5Hfju>&IdxdH8&+5e#pVT*OW z$GK;+UXP8z0X|n?+ad84?87?6>{7N~DG1oTV*|ROfe(7;-B2d zSY~XG3gr&+TFdy&Y3zg_NN|p;vLA@d_A@fubXWx<`>^NueYE|4wtlr)%j|a@hoce*zT3e*XK_^Og;$#|jToRlaYLn?1U_?71z5-%g@~bI z_Hf>I1`cn`)v=EYS4O@opRaHI7CrhkRe15&p;zO33~^N>nygWT8)Ep}840#n2Dj|$ z-w-1m&%3`CBQ5X6NY4}H@N7O63$bx*Jf}oYCFCNfI-lVi%qf(m6u(FMOZ86oD#Zm_ z)X%8S;eNY^6?C_c1%#Me)o4IIV?^&7O8?*B72nFw#Xeqf_zqq%wUJ}|4zHN{CzwET zFTAt0p>bQwxShU4hGWZ*;^&jtm~Ufaw(iAZN^;GI#jZdfY}7A+D8W%)a%AQzJUq{G z9>iJV6VBc|^E|4ZO>1Y$jN4)aGs95MW{TS~_H2BO@Fpl{rzk(S2e>UD=sxj+v;|*$ zHCAF*rte#D%-f#TLzS)ZDJm^ES|PuKY_n()IsD4=v^^o^fG!e)xc!3tsN1zy%84}M zPcVd`IPbMQIXAEi@5$Pp;}w8(ePbL??QTBn8YJ`-X`LhB+Bf|22M)&-lbsan+uEs zz_Zr6Mx2N|*K>0TZroC^iu6xK;>puMBg8Ms}_XTeM$GG`}YlyDSkGU(~K_qc~`* z48LKpAZ{U04ol0$YV0#wNuw*ZY*WlA+^@DAreW2yZmMftA{Mn_U%p*8q~jI|;}Rf$!j9&da}HUy(&sIVKQ?uf zOW4qM z_vetgnd7T#@tw6nxOo4QxUl3I*y~{~Rh@p07_TzAVjWn=_H50JYB1rf@Y}d^KWnVw zyHy!|Gluven2$#=XyM#sOk{l2g>o)EzdpUYVm=WpHsbRDk1=B|AO{Sb-EU>?IiKNz z`weRh*4A&d)@la*mU`x+Z=L%*^9Pa>3}X^iVNa0U+*(N2a-Kjv)qhya5zRHn@likL zxo$Mh<^Yu4bLPm&N;O)t7SRvw4<){X*CzYdhKtO#UAfj`B37wCKILx6`6O}}Uc6?- z=HU8W?dDX@BX?V4eXtH^8g!y*M*bR3@Eu#vd&29D)`^*MEicKmrh1%e*bbSyC$-Bv z7Vl;D1h%3M({%=8CeNdM8w0Bw>?-C>pf!IN8Q=8J*e(j?2-umuGZdV&Q$aQHal8DwdTp)r0kiRPTRG z9^ogw)2Dc_Juex~Uk!zCs?X;olf}4>ot&}$RU#j2c?bC=tAxSMGB1Astb6Z8mq#RkHa3nW0#gG=9OJEvX zO*Lf9K_dIM&E4VLA-FaeVxFw2Zm+hU_4&ZsytlYmRJXv~(3<(lBf`Xj&nr9PSB0}^ zNI1rv2hTj=@cEtKawBIKEG>(v>eQo%yv-NO@M{{&H-BMyvhE+d!Y4}e@3UTSWia9u&ttTuddCi) zi938YpgttmHQ6^d(DrB*89Zkw(VdBg6)xC*33Ig?%56kkc&uG!9}vhm=7D*pIx=u3 z{GP8_KV{IRm^S6=fmAk4!vpQpd8E8KpY(7;`~c0I!*sIPf@g$7kMHnbDL0z(#iQYM zXV+;rgxkZjzAdn`KsUgXJE3NI;ruymW9l@g$@ww@&0BXE*oa3XxWx_5H6>gT&cV`M zwEI!V^aOv6IKME?I@g?wQcrDZb2%hG7uDS!v2nYU`$(Ahs7E`TgCe}?UMMkWg+76K zShYOeaHgJPj**oi#zn?Dvxxhca^)$9M>6C2O>tVauCl)$?YvSZ$A)+`3~bq)XBYAP z^KC*n*JKa0;rj)BPiG&_8;hP2%6&VJqb%oLXfdbv>WN6+d417ZhrI_5cA5GW?Z0qd zJLT3{oO6k_%d_UhGj8O5#{O$F?l-luFzzcl8nH;Cmj?S8;QV}jufJ}qFN_muFdm541X7!ZI7ott*p8%*{L-hqPwYdB z*FK(y(GNiOS*AVpc@W*c_!pS-7roSK#rbL|@er0{#L(#DS~$n|gjk13XX@*kt65PS zY8Ti%Gr=q!V%;U}#Wa6fpD+3?)*V`3<+(o~i@Z;nD&}Kqmt$vY)2Y3ty_9QeOXB*# z{fqjBe3P}yoXt<_qL1F350cNV#lu>%=jczb#ouTxTE6I9|41!Mmvk5Ls*P8*H$MBW zJu7gX3j6U7YgD@A(@(fQWwj^Azil;ioa(zgs&@^kMws+FjJ?s_xu%*B)Q)L>Df*-} za;@NcK_1?OaWhfRAuo4auku)OojzRCA2Od5`lB&tI23LM$_wzp)?}^^u;bgde(N=n z_$=ajw_MH*=Ig9!?9>%zHrPd*2{N~Fu@za9r${)$90};WIsp_c+Tzf;Xsq+9!>k< zf6xz%1=7AHeNZ;PSn|(d%|D0Eu8j%j&L{hIE;-U#^}-|)eHk*28;xZfCWvhr(0&v7 zj^qq%)bG)aev$g|bCNUAhP7nF`GH4e5B-)bdo7oH^a+`|{jp5pGlmCkb~#kKd~Uhr zsGcBcij7rcel_A##x+E1E~<7yBR+0l^d8%TePud)7K`e0YtM#kTP>>F_?l&t*C%M_ z*_z(_71s{dCnG=1t?gHsSF6OXNnCv-!$acX;+mCcLb0Sd=hKCPL)X$3S&M|Be->(2 zZ|xD{ouBixKLlV0Z~nS6}&y6xe02YR7$L@(IS&f&BBWdF0XJbbqOU|v^0p4XF1 zUu|D(TQj<3L*vzX{0{ozV6GhA=alp!`P!ln*RT5F`i&0OZ`-Zk%jo#}ZU12Hs`mcc zCA=zLyH6^SwcDq>@wkqKH{iFT8`p%eG5+RpoxVG+ZvPwGam^=LnO+~Rk^bI(pbqzg zzP+xSy>3j`%`(@8_PQv0U3_?5vuLktX0N-->Y|``URtu76rT)IVfDpZ>spww!|}YWr{8Ls-`Pb}V{47Q7ag+pJ7VjZ_Ou zkkY+lY~?dndwQDUq@l|*Y}Opsmhdn-dnK{C^fwd-hqffWhp1h#IK0!`*5dHOcTx8| zwJ~}+O!BJ#lYU@q;t3YQ`*OTqUeJPyV{Nm*d$fK;wRh*ALAZ2 zcKWV7)r2GTd5*@6a9A>5T3a~X_zaeCy-`SnL&i5%zI(YBF^R}8yU5$Te`JoK+ES3STiQ1mgSdz}8k*W5=eHT6 zX+XPBd(^0&lJYIE&0K?uXMN5OXi|KJ&isY8o{*o0-hW&3kzu10be4=BbvgM*aX?!T0bN_ zw`tCee%p@LgZLFKP8ITt=&Se7&79ZVKB#}THmmqq&>kKu)H}9MSYK-b%8yblTgy%M zw~ZMrT8l&b*gwboAgt3MpAgPf$QV)RSE^G^$ydtNLX_Y_$ef$e_&e35QN3-C>-+_y zRs8Sg7m@MVR(JK{;zz>RS*s;+8mu3AFhO7HxG_(-EVXc?MKhaf22Rc?IDEs zM!d>yPm=L#WX`m;cYPJg+OeUvW0lJncVm8`eqhsBf5q%Qe+7+CB;!T+0uJxNjZ+)P zDYD~K&5V<2NUkhgvv>C6{N0|jXE652XTRvLIQzj_fa-{6Tu02cdN;OaBFWtMBDyO* zx6wJDVpiu}ZRbGEkNFken}g<&vl~2j219p6xD|oyozY`rGC2NC)^T^6sA}uz(wb#n zd-(kqqSE)MVUOukg#$Cca z`GECh##gjvmE;pe+eEM(1oPC%H=YTU=%*KYe_-EVlXDZ{ma-jwz87yPByVlqc@{fW zKkVDR&?3R+*z@n^HwW^KSc}yQIZt@GBx{3Hy*!=)j^nTTPXAzBb)mK`;plsd`-=87 z?7Ou0l3m(`aT|KfD@_&>;pS+L>0({a`Bzo#&nSMLOgInT<+M;u3)bUBoX6d$)j0oE z`NclQ%PkjpI#y!4!@W7myPKcNVjFKMomGISv^z59sk&r2b$p z(k_OFyvy(HN11cg-?QI67Weu`HIIzvRkJu>+r;I5F`kR_!6?dGW!xQy-QF}7H>4_D zB6Pk9SVxM#m&UkBHUzG#z-NC2>e&~twaKQCeLj>k8QPkbTWcMW&gzpdleo3mZuh;l z#QC%9faOzsmzwcSDwwy(`EkDJ#4|(Bud&@Ke4TJk>M#zP=q3GO9LcPurVDJn!#EOR z&m37JpNInoKbl{CFu&T(@6%ZAV}1D<47Y@NwX<`MekO;nL^WdRcV~O@ zFnc2pd8O<2Ty1$R*oJIUKAi7c*&tnF_s^#I%M#epnp~@9eJh(&*>mkYg*;EPr^-Br zshn(`oLK+2#e2g0qNmQgWAv)BIC<1h^Uk`s$c{s2mK`tW!*q4P0Wmtx#qjR7^ey-2c2Cf>G(9=_()tWNjd256LY>x4BD?5zq-}Y`|p6W@%+%Y zbj0tP*Hny`iqit&}+UH z-znwOb_w@RFb&mA`VID84>9*RC)RVAdv)Osof68&yLUeNoFjl&ysh|wSCPuT#7r)1IFF{Q)8oKZ1iWwMsdIO zXUE3o{fC^J3VOfxsO#RL-j7)COTQ7nkZ^JNUW4+@YdaGC6e#9}esR3fl$+7&Yvxj5 z+{5r^FzWw)uI;}JqyAwMtDnTE?;c>(`__)xWXGdpV=Pp^m%#$2*iuCK#GS0(z*?vj zANY)70nt6^i~@4W!Pqe;wLBYfS+Sku7wn>6G=nSLme|rh7H~C#3mb;Q&1>vEr1uoo z!G-86wD`0SSl7nm4r35jdGBZwo|kPC^$GpO{yl#_M_ehY8NvHDnsFg?G`JETl4g3Z zdv`agDZ;zmYPNp^W$jwR_eN|0<6GbTZX7#~L5t;QuN?}XYHIHgz9{Wui1q)G9TmJ; zPX~5XTkYFXZSCx+zWNcih444Lo9ojQuf9oZF^gE+D_j(E7jVJ00}*@;*<& z22Ht|U}I0Q(=8vA4rr~_-ox3@RM)q+F7|V(HOd?2vfi`onLXL766VDl+3xPiw%}MG zMib&5MdT|US)O~w;b8f$3YM_6gRjA{D8mjv#A~;zO*Y6__DE;vAl@j~!q6TcIZWs# z-#4*-3#_)e*7kIuW>PspoEek`x?Ls#wIlh+hirOmQSz7WY{W8G@7rZt!l zXD{luR$0zV=s#64ro0e@_2US9GNI9A8G(|k6wb%e0~s5Xk%6Yf3|o6Pun zHUoQJ+wpnC!2HkMdF=vBiCkIE8t?o1&?o+mzfb&nP3!L_d>E-I3(Q4V;(4?8eT|=a zfBRS8ABnH?gZIBIKJ?L+;5;_Q*!8{~_zqi``~^HerpbIb5oXoG){KvZSug~Ru3f_(1!I_Dl z`)`fm>9;Yw^DyoOxR}V7@=qAIdMnybO%ncnZda@q9Y0gQyJ- zbC+%8Q{g|b@&CU;4!@H#X190V<~s4&R(K)9uIM(zws@v-b^Hs7U!%BAxK=Ybix~0C zqyN{&@Y%;jv3Ih0yHp45r}YzjG|5i6}PGSF7m208LwicRr9Q2iq5s#%7 znrgnV-ahiFrlC(bJ5ncCvvqP@BTPAXbyvUVVvdG!t_7C|*OhN zo5}d(%<~;uJ|&;|JwnF55*gxrjUe)E-BCNmt1^UFWm3E<;V`B47#efqI3^p6#cp{t z$hSDtR2%8I8IZ%kVakw9!aJI5+x(~ZSGoQ=ZcdW3uJCb0Ag2lD_b|6*$J!Rzj^Xea*5M1xb%8N`#ke%d zHQU&;;EC2f<+S5j(Q}-b%{RKXUAKA5)|S9H2A;tsmLa)u4IQTVVsG_e=SO<5d9M(= zm-lyLdDd#|qrGH~CD^utSUDdZjveB~pl<_ny!l?v@upZH`ZzFWOsW07ro6Y;%#_K- zYxd?#bFDt-7IAEyb6PJgKmFUe34Sy;yM?cXGcJm{tfMn|x|CNT@d?zk8_rG4#2%iP zh}~&zlD(MVLgEl@%l#e-x0cG%su&s zsXjHbXE2FVHY_$eVvZF^FLFDIWIx!S%;6F6UTf?*2+yXH+g4@S@7CdbhOz|bZ(8#U zpYe#Q?maHgWR7@GVQAOi6Rn|;e(uM5HzE1T{Vw4!!QV9{*z}ljQ195aq1*oUxq`%A zQ6IihkIlhd^?p?AEy8;HIjmJTqgv&*f74!ubv+y8d#|e6SjPEEF1~9m!`jRJ=z8hl zb@5$qHmEO>!OiSGE=0-T4)&CKdp*2qCZodmX>bEG=Kj(8wF!b(i8TEgsBJSa+vcr$ zZ3fY{b*1vnAl_aCs?@wvFU>2nYhE>9T7k(mulg^oS5s_WEq2Wyd}-c>yXI@?HE+AS z)@@j5-X^==9{na>qXEkwF2ceucBQmXnL(6%C&;N*LsbL ztzeOB2Hls|RhVnOcD>eBH`luD7Ft(H?&7MyZQYuD^S1xmx{dO!tA4(98|N>AeyR1U z3eBr{*LrOhT0xv^z4i;O*SOevT@;%^@^TS`#pY|`T?E}?>ozG|1WEDYYO%ezGNtD2 z;`QPxDz&Z_`HQQ#bP+5{nL$#~5HmiW@FaLz^@Jsa2C%h8SY z2G>zCu9cH

aR1b(6SOeI5JS8^Z5{>k=%?(sNipHvhfi5TCcxccTJW5;c&4ou0uc zJ&&(T-Z`A*uq~sz3S2wD&lZZFBlp)E*pA%juDy)!2AfeKR^8n4N}1O4Xw}~JuUmUE zhJO!l2D359DLlJ7Dzs-KUteAKK-O-0*yj15Lw`T1+ITa{H}Lz#2;?`cm7d{y9t)*A zS6EwsGD>Z0<>FOi3Gdv%c+L8S_Wk&-t44Rd0=)BfROmg8s}*RE2iNzOgL;$3tA7XM zb2C_u?p{h^4ewdL_OBZoXoEL`aTwoK?g#MxF^rKi%ct>Obu)yqhG%%=d}TM>=@pDG z^u6j0uPaZ`r%QSkjQ0S>{)*O51KQr2jpiEGS1Sk4-i~1W`uXMZVB8O%bk;21p(uE#Kr%lshHLATod%ypfp>wW5Z{p-@x2y_9+Ak0B)hB6i9bno0N zReL^-&Fkc*bwA3;O1vwo=my57)|;Uo8RbhLXO%k`3%F;?_xt)eo^v}g^F15YUY`5; zD$4N_$|}w?%xi9tPZD@`_WK0o3uO_W1M z;~8X_p9TNLh2Aor7n9x%u9IF7*3#3!y+hxRRxJ;o@q@X3gfRrYi)*n0GSj*r+<;71 zm(SylUiU%fP@dDa@5#aQ7{(6T@T=o_Ke}sSKVNw~ZJ+kmk+h))^W0kveO$xoo-vR$ znH@vvN6C&!oL@eVpp9MkdHZ?b)+6hcu-VdW9p z-p=)5?E3p_h3Blcgg(_~JG5bw*$SQwx~Mnp-=U0b)b({Gmyzw)0gPcE=l43dyva9E zo*zK}!5DM9`qxDq_hqU>;Cgvhl&?Nfe?aFA>b=`RM!x!YJ(#n`^6>pf?xA%I*Q0(+ zCeYW>HeF+&8yDkikTuhadL5X*YiQd}pMWeq9~+bIqS^{;&`ub)uznBvqO9N$j*fY%)9AU-=6?QVHUvEh>uu97^fn+5_oIAquV40>(RF19{bRjSo1xtA z1~=8*;0D)7q`HfW4(WZ=2dkHp$#u_DRh9{-pM6bx&j_D?-7eAi<#E1yJC2yo=(1$UEJsI@*~Cp2VHzOUJtMDA4attjEjDWVgDK53<{I_@}#|I^Y6b8_RahF zriXpF?StKidJS~^Bks3@71(QYg=@!y_4OFnv0pi~PhjtepcX~#~1&Rh&-jia3oZMwt0DtrdW80*VQ-hJ*DKP_j_A80S>RnkqPd>Q7n zf`6k9BO5NP4^W3LVC~FC*Ih-n49FSSv^Y+;F!wYbs8@09MHX+Jt;zE$b49k$4EhH; zCWm{!$gKXZYAvu`a4zU?Y>(*iBHOElF|PZ)_kqr|`yZ{nc&n%~mb|}sip7&HExj)O zuSS34HFzYaAOxIw=~skWR82b1{pwk)hWfRNGQPy~NFV!qo!gG>I;PK~Blk7HetBF@ zdKG)`NmzrwJ9a**JFqu#UEN}vpwH8+T`;IsmUgUooMO~TE8G`wPX>8>1pBB2?Rd?+ zi}oFwkE~4Lpwajh@qY+WkG>y;l>iE}v+!>mKTQ|)@PAbtKON(*ef%FhZ#k*!CC($v z*Ai?moS%DG53r}qmV;_#J^@_@ZN_~D`<#PopFwx8VEtwFB^|A6^n98hdPyBGsik`u z{Die#B|8Jps}q|3?sqz`N9tT((&YZ{FaP`!ug8;b50mM)c>T0`{PKJ5pI@f)ht1=+ z(Q+LxzCCRw<1fD#|M}(l?%U(;e)8pakA8oiJZ$Fc75w?fKmPeIe#&gPnSJ^F%W#-4 zO)KN_$w^`8d4*i*q&ym*p5{uWVX-nE<#J^&SIkvP!^)^wEaeMvTpU&^`8X~Nhoxye zmPWyoU^%~zA|C8*V#a+M&&Jp3Rwt@ZfuGf{ebxIqU6p>DCh*y+@*4=W#@{vkKILnE z>#OQl_}okjCAkOFa4Il}_Dr0FQxaditw}j725{S59B>Qe-3R+usv;lzdE~6FZ}?PNInpA;|A0| zi2rVkt2z&AG8upgn7fVZ3s})DP}Th^9P^*!70L+x-MYzDP_)s^!J~~gNbJ3{DLij6 zsJHK$TTm7F*{sUP-&}=MGpIS)eqa^QJuhSigJ}fIsG1vtZ0d;KQyUfrQ&dMVYux7R z$NUYv?*>#bZa*W~di9pBZQ`|{(x-hmX5hAtYz%ZyP3|d(nYjfO#qD|+FDv)sYK7Eb zysXU8v>u={n!aZf9G26R)EB6d;mtifcTLYVYIe*)+yweAiV0&qa%#d)+SH6na~17mVX2 z-*a#K14ru_=0?#wed<4&I;|zl5`WgKOyTeK2##YgPi>Wh1x*~wi-z9DzTS4V!8Gl5 zvs-QVwSF98dUEVR5GaYE*u^}rPuWaOs|E%(fO+_7C|h|Qc{JZiWQ2$v3)wV!PxflD?B7hpC*L)GB$ zDBSPTyBKZ)6uhJZ6C~KBvJ}4lHdOv=KO3@;@>ZnBq4b3&r&V#uHNjJUf59?mO10S- zEPXB$)m*gQ0dEq+~zHNL*1Y=2V|0ac%Fsvuvq2hfiC8f59)E>>{(MF(=VQ$8l_VJx4xH&9i+s|bPsw@yMOpC!|P2pIi zlQJ1dq(D*|rDWG=SEj>hP1rY;eHOnnq4D-z8p7ciNqH-cldC)RHnoYuY!sU3@*UKrcL~XS6e@uV zAqd_~3heV&`RBKFbIIXsOE2*_zU(}$Ds2k)FD#|XZ>3!BzC-D-9ZcdW3JggDX$g|m z{q=oea_gz8*?SiJci&vXyE$bux=6|#@@%VOz~Os%mUl@h;8}W$oMAnem`KAOu|PbT zRoO>-L=}iWF4Ae3lVG4V&MncOhk+||T3{-HUR!9^KFf7p-3eST(IZ6+SviMwLXedW zrnAS?vxUIuEf@~4;ZjdLXu4dY|>Ql(1-9!m=P(4;%y7 zfnY4Vbybdt7KNX)pyGL^8ls<3U{pLLs^Epd8rpjob=q67-D@1H3DJ?vION>W&_|>} zG@9FJB#o6mk0w)EGf_Vxy11y{G{c!%@jF4_C?&Gbx{+v;lug;SunSS0U~m>NmQA4% z4j)x*=GOgL)Jl5F6TQ^<+LcntlcwSnDR-~H#t=14E1?ke6_=SY#Apwf-uhr) z@P5!%W4sphk4LZw2rGnZNhKDZ3chPJK|**P_0N4ybA;zCwp;?lV+pngjU_|ZbKhB} zUCEn?l!VSn-d}7VvHXzB;e=i-;_@=M=2ama9m(y~59?gcsi>qNq7t2UIZPG>Ng+{Z zl%iEDt^=efm1kgSOMOcvE{fk5F(hN(Qeb4)km6KYW2t}Zq3U%XZnjeHNF{`n-pjNJ zi^Q*X9+7HSN=X<#p56nJZ1qn()sh~{V1wMe zEmY{^kcyGg?1erqipr9rZ`WS+$l9y&+UuISr1AL->xiieJ-xj&n&`yoz4t_%#`IXWO?HCf_uYR*RCN+>#(Kci&`K0kcahWxNWpQ$>;aFRAz{U8GYW+$+rF7 z_%WqQ!L^v8Baj(}XxNmqADryQEW77rH4oD@mCCJgGEI;N3Gest%vdXW=R!(5W(Q?w zY3hri6kML8{nRgb7LBp;XpD)Pji@@Deq+5PazytbRJPvIsq!flbRbzUn8B!QF zNbYt>x2Zy+FjO<9F!Y$h(8m%(lEFCfSq2*^h2en92`%getZB^@hDOria14E+DsU9b z_$<&wr5+afYu<#%^8WsdvwDOjQ{ z^s_wnYoY1^*_zQl$k#}jOA6C7SIU(YplHrrn%?K7Q9KIF4)%xVLG;f8^-q`K zZb|=O*>cGGXF==17pO^0O+Kc7f~@|@wCB7w&*&jOzlBt$nbP_2nL?`vIzx!uZz8KR z(-cppVaD~c34GXti8hY+eug2MX{K}B@%KK{64~!V{+?w|cd^fXrsSnQBW+xl_q8td zx$4&ULi)_qBn-)HXm5{blHlLrUxA>u9etZtWy4Z9P#h`Wmlka;d0xke6Ls z3QDk3(b?So9^vnrZ67%k+cb2x`y6!**?qWwZJ&c|%4L|s#mE{}HqFwd$%Cw&G369P zZUub)*D{A`O0cA}LccJ5wF$QAg-wkP3_*fwPUm-XkLl7O%UzY^!>^;>X4q4ks*5(o zjP2m>O=#+j>Q{!8bhaY;ObY5ZhGtbeO9>wGnSFcNC!4&D6t)_5PaqV-A(dI<^Vqi2tmiSkn)H0I zXWLN;KpWH_!XdPdtos(*cAm5CjHQmk{un=}7eP58nn>8X`Fw+XoKnC@!11DtJ%-x90Z?70hQ$rngPh#6FvJrk8>kX zwB@pO#Wa9jM}rq=6{(T#_#}0 z<>c%zv5l43VVxvT&-L&`MdC)?=qJ3m)0Z}Z6&emd@o;K2apCfce4XLORTF91?5Upz{bgXzzC7&G1 zOLPPKGu}u0^Xppi{@lm=&TqHj-hLc9$SbxXV1QB^g5evEoD&ai2$apkd)~7(D_RWOOz-#f6gx%(AfmbjI2-3LC-TS zL1bZVmjpLuWvIxov};AQ3Y`r7#G(d|0o`UxcvD)6&_MkVP_FvW(?n)@c7S5;5^Q84U4wa)toT*n6 zbcZ%}a1K)?aM~~yTZ#u_3D2I7F%chQ2jQst=RJ;%X92l z3gsW#(OO;cyN~E1p^3dF$b={8DVAv;%2+76B@CDrL}hfW$@qyr^2KF!?BP9!zNG{I zomY!4GjmbZL@^pOR2Zz;8vC^6?0>EjQ_aAWzp<+?j@Z>rYgg|QYgZ@Mu5P7vHPPH~ zX|ZCTH~R9>E(=1L6SU>D__Ser^l|RE1TEQwV!z?%Mr_x%rGZ4-p-wSk+_TYs+EHAl z%4g~(>WaCK{Rq2Q`YyH=R%xM$|WV3K{J7*1o$aq+Im>;v1B&L#|q zAVq9KO*UbTY{J`wZ9;)j1Dnu6<~NT}wZLWGm%bA7mo+Gq5 zma6l4p*)!}i=b`mV(8Z}^j-1utIK+0Rt^nb5tU56|u0 z7N!gAzrFpOsC5{!((ao`GXs0?wI9}po|#(e#OGMzK=d8?#Vq|$-NLdFeG%-h49kWz zHYvpl(X@0Ny-IZH?7V&oL1<|d+&TG&^e#b|%FaEs&C)C&jZzovm94c`hL%Pt@AiJW zA1Tt#_zV)Aze|Id>`^6shHBD{*XOAw|5K=8zHXEMHfosO=c!?y$3LLLXnukU% zcJD+Q5#BqA>Q1QOK$nGdtw(2!ik{mY+e{q z%!h%%z{~kXXbDKi5Y%I$deL0ZHVi99IZU8CW*g2uBA=DOq;^!mx}hug5#$0xJW^|g zhK1}-)MsJ?XJe&(MLt2?kN9lid|2Y2YG=@=2Ej`6_b3w$w6W>zMi36|)bED(HOaq6@wJ)HP$cUxVu;_+r}h1SV;gY(0^H}( z*z2|$;l6|Md$Gr5{5YercX&gi=xd?&HeFxpz*TZ`yoqDRUps1eLf zLXg$4Pc>KeD@*eKDoGXQ6H9(oHueRhjtHqqVRU1(blK2s``9hlV zg*fk0bG)g_ej?7h!1b@y)AJ{wy(0_l9og@8?z_-_XY*c;!Fs=iNXHzi3>-Mh$sq*z zWDf#7`4tH8Xb%?s*;f`KPV5SyVIkki#ZF-5j6hG%bYNl9={tmUG|@*;-UnxrUxo6< z{if>VV;{=f=APl&bL&c{a)?CH1JF zj#5&80D;@ts!Wb;D+3YJba7VljnQgM{j-w4;c}IQ`Xm5**}f+f%7W1U4J?hMjqw7T z)4_W#L#X4~ljN@Lq}T@(v)_n(K&mEj zAcU~yJ#>EP)62qpYqtMgK1267y!Ukm-h1$>_u#!FnTvPfy`AsRd^Fek&GQc-5|aan z#Mx(PBOEQrs%d_f7UUy&KNES6x6z@zvu^n{dH=x}d?4=+3GC#DXHS<0;?{LQzTl+o zPm2hJd}i~e;XnO#M!r0KRoe-quZ3j1yVn9M=is)C5n1Yx@4HIAZ+HEXd_<|A*o#?b zuRrvY+v~-KOLhDcoN(XIrP_xUZ<3BQ&Yo{G4A_fqx%`n#yz5i3E<7K{=Px|AocCY# z@oWia4TNXA-ehyDF6=p`dO$r}b9=STy?TD-+^bBE2gP+HCror`Ixo!+=H)SkOxqC* zw)krrIw-WhW6cZaPrH1KD2>VCJvbLOheRJdiy-?6MsHeZGJTAN=xh_siPXn5*~YvH z@fylQscC%~CXVVsXW|l^GvZTQ4`UKFAg|q&ZZdk%`NfObn5AEaep$2XWc$q_M#c!i2lLnv@#wAa^M0&Yf-%-@meyJsFBA zp<$HR60R4b-zzM98jA(hK3T}W@9r4{l^g~#Z|D0l;Pm>p{kOR!uK#l0Xyf@Tuuy$R zVnqSZWy{8j&J*=EKg`Y70x9d9FDZ~SLe}4i(0x%i^L`ft-hC!-56`7_Q)W2Vkp13p zzSpYl;QFdj3RSo(r_^|~HptgM6-a4~Ig0Am#t=iBr-%Ct+Y2W#!{V<>L<#K1X<;EK zTf82jZ!)ggoWxI!tLs;qLt-+ya+jZ*R~tVvH~ZHg=GD3DFIj&)H8>9TR)G?wZ=tNWm+wsHa@ej0zh{wMZ#S~uUcP7auy6G+QkI%X{Nl4{pRyFcpHW`8TaS0D z8R$CJ{Im|_kaJ}a4xIL}`KNMp%%9KEY4dycZ8MZbgg8x%DbF?Krr`OQ{E#zCg+zIH zmLg`ao#9{BPClO)Khw@biWq3uBc7f1Z6&zZw$sHPe4<}A+zejnq$Og=E(VK=YGe&Yx#itDf!qK+P=a2b2srhucKl2XO3At{iYJOJ9NRpt5S@V zDG{CB(YHPW!+FVI3%9emtu6XKlOtH;Izpycd9g6DsM~04qpOb9Z?spk{88Lf)sF0! zG7kegS2n)7hVh)EPYwFNfJ?zfTu|)N7n?my?%}gXrm+DzXj43t^7YZ@g5%z#dRf)S z{lCuCD+IR>^7=x$E5Q4X9dN0SD^6sLAP!0 zgUfSn+uWjw%`ez~HHzOudHRNZy6et~IVlH_paJS=(Pg*Rav;GkuA?mS~N z-iU3-=e8=vs(h*|(LGp?wH>Zmf`g|$qsH|1HL987TwAndnQpry_t7}?&^OC6E)eFN zr{8C~Dj)NXVpr2#Bwf)yVwW7&{NY-V*w=(os4 z0T>sPf%EoEUE=wM>KP@6S$E;{#inKJl{rq@jc(?4KQnUHqFk|E)FEHnUhIhSWNqw> zbN?fzp;MPkwSvFviPfQ`Yp=xjRBz}r_CFcSqvsj|qLs1nmXO|!v4xKK`MAEG{QY0K z=B~yumPT4rJtZ10__^`9mLMdWrc6y}mFmPehe`Y|U9PL)_#yixi(CC31#n7r`ZnGX z9;Z9~Bn@z6{nIKN+mLuMuf-ka$6@|~W*-pKihvlE5By*F(aOK5$j7?Tm45_`VYT$k99T-A=m+|kCXavYm$D!H~e6UPo5B@~UdeR+M|Dn#7{6tI5X6c4WH>bLOhI%NU`pB^lAbZ>Az8@hq3e0yD z?$tghM>%I^jt+|Q1`OLrT*EeZ^x<>rPhjaR*f*@Ot{0x&=Uf0jmxXYq&10J=@5tIv z9gBIxG&{PJ*6gyQ+6@5$Gg$T`^XzSw0v70T5EI|*y$`CiTQ8(PerI-f1#L|d#J zq=Q1)WFL;KpP2sUT&CFKrpP$NmR_pPVeQG#NXE>sAm~2KFLrFLHjtw9M2v zA}cfo=&PVT1^uksnWMpEx<1}ZRl8dKC?#6?1wKLBp+s|SrMK0gd;@)VsvzmO4CwYETkHK;GgmbER zUKGodwS$y7kf$tmk`e0LndGm@dZ78XZP2z2TI@<5Td6hg^7nsnn^Yiq&OeYTIj1{% zL0fm=qNKG=7_c2b4l_B<-3a$spU4Imc9zi{y%7;3Xk81V|rrmi4 z=|^6wY>RWgdax%uAOjA^3H=%2p3G!?@$NnD{E(0jOgn5O#<4OIJDw|t&!lZ^*~mKc z-C23W0c5q=q_RcXR14C{I~yw1(y)q>hV zaNG4Zw~=AXks2d6u2lrCKSvP40@wQM@UnHmP(-=|3)zm4x!0u?_Bp;=KK5@A?wX)u z3&UFPDXit52eswh3Ws=w*FwAT468QA^AS_^e}5YOHDCD<|Mma;^B-SU zPj}xQ*4xdO-wXfv;~)S0vf07czyBW3hV#|$zdt?A;@^bL+mFN8q|K=)R2(Z?@@no1Q z4NJpuxiHC{PAXF`Kb=fUrOIhMJjsoV#md;5PR51GxKNlDE0e-WE*BTa#gocpI4)M= z%Fn-XSjrCzmEy4EP4oHUcB?-A>}igUJx^UAh?FbwAuq9x{I~COh~mJ{@_Jq38aAFP?|X zC%iHJa55Zu@o9c|T8!hus4y%J3#X$(`6O2=<|}aXbPC9{;C zQ~=4-cD~pB4+pnhHZ-^zz))Qf1Plr?ZYf=IUD@~!%@hLnZpZCTEZ24*V)T`u+xxz388S%XQ>8SLHrb^S^ zZ|-R9!E{$f`Qh7<&6U0QWID~qZitJeLa9!( z&%;%@H&TE3=-aT0ANJ`Y8NVuYau5-a0eepswj@)eej!@m!f6zrmvN@ZBgz`CChokaQFMjkr3{o*a`TQtM<)6P{6h4^yKRefDFTeNoUm?ne1N$?gjQj5} zjx&uuXmCERO!FB9`liFNw?D-P-xoaC_&>P+U?#oCga?&I4X@MW$)zl3X2R59^=u;X?`>=l`G{_ zFkGjT{KzZB$1K|YqeZTC{A@8_jVCWZK4E+?+nY)J?f9BK_$P-Cc$j~CS3jj)C{-$_ z3nMsPXXYYIrx{-NdWAGGH$ z9_gPPoBN0Lc=Mk$I0p^-TSo^PJl^J=hAZ@%8Xv^%Gr((8%3?2J!t)(_L@ zdGvDfeD|bFow)e&u)7})>&4l6)|$3HP=g;1jNSgTgTr5tk?`RF9bcz=qxAVz`YXd! zu3T2mUmtpdrs|v}wQhGF%`RKr_T9itg5p|7=5kwKnA^KrVe)XbJzvfLJBI1a2*FDq z|6PLAyt+AEH1wc*+FQl9-NoXucJ@%b+^i-~!}j&}#qDl&Gt3uGS63&|!{#M^__LP6 zLGS)v0VqY$}l0C*S*etZD`7s`t$4HVK%JY4~tP!nr(0LyT+d} z7qDfMmw(gDoh~l@>chITDVf*TW})BKjr*JPmxsdUxi{&@FQz(uc)p4*C+$+dR4tAB zf7-@-)4#uY@s?h%d!zQ?#h*rtT6z8WWY)RSY7mr5CG&j!n0p#GYR}icKPXop&K^6f zlV6*=6)e#4a6I1qyC$xeoHhK*r(1K=nQVh?zq#vea?iIngI;j;xEY>Z4R^1_+U@0L zu%16XzgD(?V&aZ;?r)woeHG8^pj2n~qtnj)bF*-Je|i!ZHm!ISJtw`!$<@tyJGr_3 zKCMnho12^97gt<*=Kje^%N2fQ(#Frdn~Goi?mxD5CvLoMt{>y(CcZb@+yjVUYjiU_ z8E(d}!Oi)VdFaNyU)f30KK(WO?VB>eC$qG&R2_LK85|Kct;)rL$rQ+h$rxf zyZPfL^TAOU#hVlK zgPSH#DCR3CCH5K~KfQjwm-y!g-Se_d^Rlh-KJL`VH)i@$@rp+};!ZCAlX_A(Inooc z#XsMVTivzvK_TZ63iuIlg%gpC=E{3(14~ zNEubK<8fLpoECE@mE8V5ST5i`xc|(LG}PWR^TiXd2+uv;+ZYSE{2yqe@c3jJ=%t@# zUICvzt{;cXZ+mMZ=RV$B8R^FvsX8fCN=2_kJNEqH@kyRVLy0nnoy?`*;REHnyjMCo zeUm77Eb8*Z8G`|s&X0=2zrMIPegl&g4ARHR!-j0j;X^W^W{|qN8@|ZzG*S<9+--l) z<@u{`cTda5`TcT^>n}&wz&jrwz&?b*c%;dKKEOnhy;y;n8c$a1JD5270B-q%67+_v z@v$5Dk$bmPDwRL};I;Zu^I=t&OQ+I){^R67ssmzOr&9OL3jB7y8n3sw5MXQf(kq?R VzH{X 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

m$9(hhBF^$q;$ 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�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®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 delta 8306 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 From 61906f88e942c34459789ceff093e75168144f21 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Fri, 8 May 2026 09:51:19 -0500 Subject: [PATCH 29/32] trace_api: emit parent-trx cpu/net totals on get_actions actions get_actions previously emitted only the action-level cpu_usage_us / net_usage on each action variant. These are per-action and in different units from the transaction-level totals (action net_usage is bytes, trx net_usage_words is ceil(net_usage / 8)), so callers that needed per-transaction resource totals - e.g. PerformanceHarness's post-test extraction - could not derive them: filtering by action name drops sibling actions, and even with all actions the units differ. Add trx_cpu_usage_us (uint32_t) and trx_net_usage_words (fc::unsigned_int) alongside the existing per-trx trx_id, block_num, block_time, and producer_block_id fields on every emitted action. The values are hoisted once per parent trx so a multi-match trx doesn't repeat the field reads. --- .../sysio/trace_api/request_handler.hpp | 26 +++++++++++------ .../test/test_get_actions.cpp | 29 +++++++++++++++++++ 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp index 2ef44a490c..215bca6c0d 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp @@ -265,11 +265,17 @@ namespace sysio::trace_api { std::ranges::sort(matches, {}, &action_trace_v0::global_sequence); // Hoist per-trx variant fields so a multi-match trx doesn't repeat the checksum->hex conversion or - // re-read the same block-level members for each emitted action. - const std::string trx_id_str = trx.id.str(); - const uint32_t trx_block = trx.block_num; - const auto& trx_time = trx.block_time; - const auto& trx_pbid = trx.producer_block_id; + // re-read the same block-level members for each emitted action. trx_cpu_usage_us / + // trx_net_usage_words are the parent transaction's totals (action-level cpu_usage_us / net_usage + // are per-action and in different units: action net_usage is bytes, trx net_usage_words is + // ceil(net_usage / 8)). Callers that need per-trx resource totals across all actions of a trx + // should use these and dedup by trx_id. + const std::string trx_id_str = trx.id.str(); + const uint32_t trx_block = trx.block_num; + const auto& trx_time = trx.block_time; + const auto& trx_pbid = trx.producer_block_id; + const uint32_t trx_cpu_usage_us = trx.cpu_usage_us; + const fc::unsigned_int trx_net_usage_words = trx.net_usage_words; for (const action_trace_v0* ap : matches) { const auto& a = *ap; @@ -278,10 +284,12 @@ namespace sysio::trace_api { auto dec = data_handler_provider.decode(a); decoded_action da{std::move(dec.params), std::move(dec.return_data), std::move(dec.error_message)}; fc::mutable_variant_object av = build_action_variant(a, da, shape); - av("trx_id", trx_id_str) - ("block_num", trx_block) - ("block_time", trx_time) - ("producer_block_id", trx_pbid); + av("trx_id", trx_id_str) + ("block_num", trx_block) + ("block_time", trx_time) + ("producer_block_id", trx_pbid) + ("trx_cpu_usage_us", trx_cpu_usage_us) + ("trx_net_usage_words", trx_net_usage_words); result.actions.emplace_back(std::move(av)); } } diff --git a/plugins/trace_api_plugin/test/test_get_actions.cpp b/plugins/trace_api_plugin/test/test_get_actions.cpp index 885357fcc2..4f5afd1b8e 100644 --- a/plugins/trace_api_plugin/test/test_get_actions.cpp +++ b/plugins/trace_api_plugin/test/test_get_actions.cpp @@ -253,6 +253,35 @@ BOOST_FIXTURE_TEST_CASE(multi_block_scan, get_actions_fixture) BOOST_TEST(r.actions[2].get_object()["block_num"].as() == 4u); } +// Per-trx cpu / net totals are emitted on every action variant (alongside trx_id / +// block_num / block_time / producer_block_id) so callers can attribute resource +// usage to the parent transaction without a separate lookup. Deliberately +// distinct from action-level cpu_usage_us / net_usage which are per-action and in +// different units (action net_usage is bytes; trx net_usage_words is ceil/8). +BOOST_FIXTURE_TEST_CASE(emits_trx_resource_totals, get_actions_fixture) +{ + transaction_trace_v0 trx = make_trx(TRX1, 1, { + make_action(1, "sysio.token"_n, "sysio.token"_n, "transfer"_n), + make_action(2, "bob"_n, "sysio.token"_n, "transfer"_n) + }); + trx.cpu_usage_us = 1234; + trx.net_usage_words = fc::unsigned_int{56}; + blocks[1] = make_block(1, { std::move(trx) }); + + action_query q; + q.block_num_start = 1; + q.block_num_end = 1; + + auto r = get_actions(q); + + BOOST_REQUIRE_EQUAL(r.actions.size(), 2u); + for (const auto& a : r.actions) { + const auto& obj = a.get_object(); + BOOST_TEST(obj["trx_cpu_usage_us"].as() == 1234u); + BOOST_TEST(obj["trx_net_usage_words"].as() == 56u); + } +} + // ABI-decoded params are included in the result when the data handler returns them BOOST_FIXTURE_TEST_CASE(abi_decoded_params_included, get_actions_fixture) { From 22880d28c04a9f0aa4263d99bade712b15d4a85d Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Fri, 8 May 2026 10:12:42 -0500 Subject: [PATCH 30/32] trace_api: get_actions trx_cpu/net are full-shape only; doc them Follow-on to 61906f88e9. Per the slim shape's existing intent ("omits the resource usage fields"), trx_cpu_usage_us and trx_net_usage_words are now gated to full shape; get_token_transfers no longer emits them. Also document the two new fields in trace_api_plugin.md - example response, action field table, and the slim-omits list - and add a slim test asserting both the action-level and trx-level resource fields are absent. --- .../sysio/trace_api/request_handler.hpp | 33 ++++++------- .../test/test_get_actions.cpp | 48 ++++++++++++++++--- plugins/trace_api_plugin/trace_api_plugin.md | 13 +++-- 3 files changed, 68 insertions(+), 26 deletions(-) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp index 215bca6c0d..a6ad39372d 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp @@ -266,16 +266,15 @@ namespace sysio::trace_api { // Hoist per-trx variant fields so a multi-match trx doesn't repeat the checksum->hex conversion or // re-read the same block-level members for each emitted action. trx_cpu_usage_us / - // trx_net_usage_words are the parent transaction's totals (action-level cpu_usage_us / net_usage - // are per-action and in different units: action net_usage is bytes, trx net_usage_words is - // ceil(net_usage / 8)). Callers that need per-trx resource totals across all actions of a trx - // should use these and dedup by trx_id. - const std::string trx_id_str = trx.id.str(); - const uint32_t trx_block = trx.block_num; - const auto& trx_time = trx.block_time; - const auto& trx_pbid = trx.producer_block_id; - const uint32_t trx_cpu_usage_us = trx.cpu_usage_us; - const fc::unsigned_int trx_net_usage_words = trx.net_usage_words; + // trx_net_usage_words are full-shape only - they are the parent transaction's resource totals + // (action-level cpu_usage_us / net_usage are per-action and in different units: action net_usage + // is bytes, trx net_usage_words is ceil(net_usage / 8)). Slim (get_token_transfers) omits all + // resource fields, so we don't emit the trx-level totals there either. + const std::string trx_id_str = trx.id.str(); + const uint32_t trx_block = trx.block_num; + const auto& trx_time = trx.block_time; + const auto& trx_pbid = trx.producer_block_id; + const bool full_shape = (shape == variant_shape::full); for (const action_trace_v0* ap : matches) { const auto& a = *ap; @@ -284,12 +283,14 @@ namespace sysio::trace_api { auto dec = data_handler_provider.decode(a); decoded_action da{std::move(dec.params), std::move(dec.return_data), std::move(dec.error_message)}; fc::mutable_variant_object av = build_action_variant(a, da, shape); - av("trx_id", trx_id_str) - ("block_num", trx_block) - ("block_time", trx_time) - ("producer_block_id", trx_pbid) - ("trx_cpu_usage_us", trx_cpu_usage_us) - ("trx_net_usage_words", trx_net_usage_words); + av("trx_id", trx_id_str) + ("block_num", trx_block) + ("block_time", trx_time) + ("producer_block_id", trx_pbid); + if (full_shape) { + av("trx_cpu_usage_us", trx.cpu_usage_us) + ("trx_net_usage_words", trx.net_usage_words); + } result.actions.emplace_back(std::move(av)); } } diff --git a/plugins/trace_api_plugin/test/test_get_actions.cpp b/plugins/trace_api_plugin/test/test_get_actions.cpp index 4f5afd1b8e..500158ac3d 100644 --- a/plugins/trace_api_plugin/test/test_get_actions.cpp +++ b/plugins/trace_api_plugin/test/test_get_actions.cpp @@ -77,6 +77,10 @@ struct get_actions_fixture { return impl.get_actions(query); } + actions_result get_token_transfer_actions(const action_query& query) { + return impl.get_token_transfer_actions(query); + } + // Default: no ABI decoding — params/return_data absent from result std::function>(const action_trace_v0&)> mock_data_handler = [](const action_trace_v0&) -> std::tuple> { @@ -253,12 +257,13 @@ BOOST_FIXTURE_TEST_CASE(multi_block_scan, get_actions_fixture) BOOST_TEST(r.actions[2].get_object()["block_num"].as() == 4u); } -// Per-trx cpu / net totals are emitted on every action variant (alongside trx_id / -// block_num / block_time / producer_block_id) so callers can attribute resource -// usage to the parent transaction without a separate lookup. Deliberately -// distinct from action-level cpu_usage_us / net_usage which are per-action and in -// different units (action net_usage is bytes; trx net_usage_words is ceil/8). -BOOST_FIXTURE_TEST_CASE(emits_trx_resource_totals, get_actions_fixture) +// Per-trx cpu / net totals are emitted on every action variant in the full +// (get_actions) shape, alongside trx_id / block_num / block_time / +// producer_block_id, so callers can attribute resource usage to the parent +// transaction without a separate lookup. Deliberately distinct from +// action-level cpu_usage_us / net_usage which are per-action and in different +// units (action net_usage is bytes; trx net_usage_words is ceil/8). +BOOST_FIXTURE_TEST_CASE(emits_trx_resource_totals_in_full_shape, get_actions_fixture) { transaction_trace_v0 trx = make_trx(TRX1, 1, { make_action(1, "sysio.token"_n, "sysio.token"_n, "transfer"_n), @@ -282,6 +287,37 @@ BOOST_FIXTURE_TEST_CASE(emits_trx_resource_totals, get_actions_fixture) } } +// Slim (get_token_transfers) omits all resource fields - both action-level +// cpu_usage_us / net_usage and the trx-level totals. Per-trx context +// (trx_id, block_num, etc.) still appears so transfers can be located in the +// chain. +BOOST_FIXTURE_TEST_CASE(slim_shape_omits_trx_resource_totals, get_actions_fixture) +{ + transaction_trace_v0 trx = make_trx(TRX1, 1, { + make_action(1, "sysio.token"_n, "sysio.token"_n, "transfer"_n) + }); + trx.cpu_usage_us = 1234; + trx.net_usage_words = fc::unsigned_int{56}; + blocks[1] = make_block(1, { std::move(trx) }); + + action_query q; + q.block_num_start = 1; + q.block_num_end = 1; + q.receiver = "sysio.token"_n; + q.account = "sysio.token"_n; + q.action = "transfer"_n; + + auto r = get_token_transfer_actions(q); + + BOOST_REQUIRE_EQUAL(r.actions.size(), 1u); + const auto& obj = r.actions[0].get_object(); + BOOST_TEST(obj.contains("trx_id")); + BOOST_TEST(!obj.contains("trx_cpu_usage_us")); + BOOST_TEST(!obj.contains("trx_net_usage_words")); + BOOST_TEST(!obj.contains("cpu_usage_us")); + BOOST_TEST(!obj.contains("net_usage")); +} + // ABI-decoded params are included in the result when the data handler returns them BOOST_FIXTURE_TEST_CASE(abi_decoded_params_included, get_actions_fixture) { diff --git a/plugins/trace_api_plugin/trace_api_plugin.md b/plugins/trace_api_plugin/trace_api_plugin.md index de492fed90..ff108c28cf 100644 --- a/plugins/trace_api_plugin/trace_api_plugin.md +++ b/plugins/trace_api_plugin/trace_api_plugin.md @@ -301,7 +301,9 @@ for the cursor pattern. "trx_id": "abcd1234...", "block_num": 1000, "block_time": "2025-01-01T00:05:00.000Z", - "producer_block_id": "000003e8..." + "producer_block_id": "000003e8...", + "trx_cpu_usage_us": 200, + "trx_net_usage_words": 16 } ] } @@ -338,8 +340,8 @@ resume pagination from `block_num_end + 1`. | `data` | Raw action payload as hex. | | `return_value` | Raw return value as hex (empty string when none). | | `account_ram_deltas` | Array of `{account, delta}` objects capturing RAM allocation changes. | -| `cpu_usage_us` | Producer-set CPU in microseconds (present only for input/top-level actions). | -| `net_usage` | Producer-set NET usage in bytes (present only for input/top-level actions). | +| `cpu_usage_us` | Producer-set CPU in microseconds for this action (present only for input/top-level actions). | +| `net_usage` | Producer-set NET usage in bytes for this action (present only for input/top-level actions). | | `params` | ABI-decoded action payload (omitted when ABI unavailable or decode failed). | | `return_data` | ABI-decoded return value (omitted when ABI unavailable or no return type defined). | | `decode_error` | Error message; present only when ABI decoding failed and the response falls back to raw hex. | @@ -347,6 +349,8 @@ resume pagination from `block_num_end + 1`. | `block_num` | Block number. | | `block_time` | Block timestamp (ISO-8601). | | `producer_block_id` | Block ID as reported by the producer (null for pending blocks). | +| `trx_cpu_usage_us` | Parent transaction's total CPU in microseconds. | +| `trx_net_usage_words` | Parent transaction's total NET usage in words (`ceil(net_usage / 8)`). | **Error responses:** @@ -445,7 +449,8 @@ The response uses `"transfers"` as the array key instead of `"actions"`. `closest_unnotified_ancestor_action_ordinal`), per-receipt sequence numbers (`recv_sequence`, `auth_sequence`, `code_sequence`, `abi_sequence`), `account_ram_deltas`, and the resource usage fields -(`cpu_usage_us`, `net_usage`). These are rarely useful for token-transfer +(action-level `cpu_usage_us` / `net_usage` and trx-level +`trx_cpu_usage_us` / `trx_net_usage_words`). These are rarely useful for token-transfer exchange/indexer workflows. If you need them, call `get_actions` with `receiver = account = , action = "transfer"` instead. From 200ec2dfe48decf6e3dfd412864b41d529e726cf Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Fri, 8 May 2026 14:06:27 -0500 Subject: [PATCH 31/32] trace_api: emit block_status on get_actions / get_token_transfers actions get_block already exposes per-block "status" (irreversible/pending). get_actions and get_token_transfers had no equivalent, so callers had to mix in chain/get_info LIB to know if an action's block was final -- that read is not correlated with trace_api's data log and can disagree with the trace data they just consumed. Sourcing block_status from the same get_block tuple keeps trace_api as the single source of truth: every action emitted from a block carries that block's finality literal at the moment of read. The slim shape (get_token_transfers) emits it too -- exchanges crediting transfers need finality just as much as general consumers. Operators that want only-irreversible responses can still run nodeop with read-mode = irreversible; every block returned will then carry "irreversible". The literal is hoisted once per block (shared by every trx and every action in the block) rather than recomputed per emission. Test mock fixture gained a per-block pending override, with a new test covering both irreversible and pending blocks across full and slim shapes. Doc updated with the new field on both endpoints' examples and the get_actions response-field table, including the irreversible-mode note. --- .../sysio/trace_api/request_handler.hpp | 10 ++++- .../test/test_get_actions.cpp | 37 ++++++++++++++++++- plugins/trace_api_plugin/trace_api_plugin.md | 10 ++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp index a6ad39372d..e7ee2e746f 100644 --- a/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp +++ b/plugins/trace_api_plugin/include/sysio/trace_api/request_handler.hpp @@ -244,6 +244,13 @@ namespace sysio::trace_api { auto data = logfile_provider.get_block(block_num); if (!data) continue; + // Block-finality marker mirrors get_block's "status" field. Sourced from the same data log + // tuple so callers can trust trace_api as a single source of truth for "did this action's + // block reach finality." Promotion (pending -> irreversible) happens out-of-band as LIB + // advances; consumers that gate on finality must re-poll, same as get_block today. + const bool irreversible_block = std::get<1>(*data); + const char* const block_status_str = irreversible_block ? "irreversible" : "pending"; + std::visit([&](const auto& bt) { for (const auto& trx : bt.transactions) { // Filter first, sort after. trx.actions is stored in schedule order (how apply_context scheduled @@ -286,7 +293,8 @@ namespace sysio::trace_api { av("trx_id", trx_id_str) ("block_num", trx_block) ("block_time", trx_time) - ("producer_block_id", trx_pbid); + ("producer_block_id", trx_pbid) + ("block_status", block_status_str); if (full_shape) { av("trx_cpu_usage_us", trx.cpu_usage_us) ("trx_net_usage_words", trx.net_usage_words); diff --git a/plugins/trace_api_plugin/test/test_get_actions.cpp b/plugins/trace_api_plugin/test/test_get_actions.cpp index 500158ac3d..f656831a49 100644 --- a/plugins/trace_api_plugin/test/test_get_actions.cpp +++ b/plugins/trace_api_plugin/test/test_get_actions.cpp @@ -1,5 +1,7 @@ #include +#include + #include #include @@ -24,7 +26,10 @@ struct get_actions_fixture { get_block_t get_block(uint32_t height) { auto it = fixture.blocks.find(height); if (it == fixture.blocks.end()) return {}; - return std::make_tuple(data_log_entry{it->second}, true /*irreversible*/); + // pending_blocks is a per-block override for tests that need to exercise + // the "pending" branch of block_status emission. Default is irreversible. + const bool irreversible = fixture.pending_blocks.find(height) == fixture.pending_blocks.end(); + return std::make_tuple(data_log_entry{it->second}, irreversible); } // Stride/slice mapping is a fixture knob so tests can exercise the per-slice bloom skip path with a small @@ -88,6 +93,7 @@ struct get_actions_fixture { }; std::map blocks; + std::set pending_blocks; // blocks that should report "pending" instead of the default "irreversible" uint32_t mock_slice_stride = 10; std::function mock_get_bloom = [](uint32_t) { return bloom_reader{}; }; impl_type impl; @@ -318,6 +324,35 @@ BOOST_FIXTURE_TEST_CASE(slim_shape_omits_trx_resource_totals, get_actions_fixtur BOOST_TEST(!obj.contains("net_usage")); } +// Every action carries a block_status mirroring get_block's "status" field, sourced from the same +// data log tuple so trace_api remains the single source of truth for "is this action's block final." +// Both shapes (full / slim) emit it: an exchange consuming get_token_transfers needs finality just +// as much as a general consumer of get_actions. Operators that want only-irreversible responses can +// run nodeop in irreversible mode -- every block returned will then carry "irreversible". +BOOST_FIXTURE_TEST_CASE(emits_block_status_per_action, get_actions_fixture) +{ + blocks[1] = make_block(1, { make_trx(TRX1, 1, { make_action(1, "a"_n, "tok"_n, "transfer"_n) }) }); + blocks[2] = make_block(2, { make_trx(TRX2, 2, { make_action(2, "a"_n, "tok"_n, "transfer"_n) }) }); + pending_blocks.insert(2); // block 1 irreversible, block 2 still pending + + action_query q; + q.block_num_start = 1; + q.block_num_end = 2; + + auto r_full = get_actions(q); + BOOST_REQUIRE_EQUAL(r_full.actions.size(), 2u); + BOOST_TEST(r_full.actions[0].get_object()["block_status"].as_string() == "irreversible"); + BOOST_TEST(r_full.actions[1].get_object()["block_status"].as_string() == "pending"); + + q.receiver = "a"_n; + q.account = "tok"_n; + q.action = "transfer"_n; + auto r_slim = get_token_transfer_actions(q); + BOOST_REQUIRE_EQUAL(r_slim.actions.size(), 2u); + BOOST_TEST(r_slim.actions[0].get_object()["block_status"].as_string() == "irreversible"); + BOOST_TEST(r_slim.actions[1].get_object()["block_status"].as_string() == "pending"); +} + // ABI-decoded params are included in the result when the data handler returns them BOOST_FIXTURE_TEST_CASE(abi_decoded_params_included, get_actions_fixture) { diff --git a/plugins/trace_api_plugin/trace_api_plugin.md b/plugins/trace_api_plugin/trace_api_plugin.md index ff108c28cf..cd5327be91 100644 --- a/plugins/trace_api_plugin/trace_api_plugin.md +++ b/plugins/trace_api_plugin/trace_api_plugin.md @@ -302,6 +302,7 @@ for the cursor pattern. "block_num": 1000, "block_time": "2025-01-01T00:05:00.000Z", "producer_block_id": "000003e8...", + "block_status": "irreversible", "trx_cpu_usage_us": 200, "trx_net_usage_words": 16 } @@ -349,6 +350,7 @@ resume pagination from `block_num_end + 1`. | `block_num` | Block number. | | `block_time` | Block timestamp (ISO-8601). | | `producer_block_id` | Block ID as reported by the producer (null for pending blocks). | +| `block_status` | Finality of this action's block: `"irreversible"` once the block is at or before LIB, `"pending"` otherwise. Mirrors the `status` field on `get_block`. A pending block can later be promoted to irreversible as LIB advances; consumers that gate on finality must re-poll. Operators that only want to serve already-final data can run nodeop with `read-mode = irreversible`, which causes every block returned by trace_api to carry `"irreversible"`. | | `trx_cpu_usage_us` | Parent transaction's total CPU in microseconds. | | `trx_net_usage_words` | Parent transaction's total NET usage in words (`ceil(net_usage / 8)`). | @@ -436,7 +438,8 @@ notifications are excluded). "trx_id": "abcd1234...", "block_num": 1000, "block_time": "2025-01-01T00:05:00.000Z", - "producer_block_id": "000003e8..." + "producer_block_id": "000003e8...", + "block_status": "irreversible" } ] } @@ -454,6 +457,11 @@ numbers (`recv_sequence`, `auth_sequence`, `code_sequence`, exchange/indexer workflows. If you need them, call `get_actions` with `receiver = account = , action = "transfer"` instead. +`block_status` IS retained -- exchanges crediting transfers need finality +just as much as general action consumers. See the `get_actions` field +table above for its semantics, including the irreversible-mode operator +note. + **Error responses:** | Code | Condition | From 68811fe53e44d651336146025c56509645c7c176 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Fri, 8 May 2026 18:08:38 -0500 Subject: [PATCH 32/32] trace_api: populate cpu_usage_us / net_usage in make_action_trace fixture fee1815746 added optional cpu_usage_us / net_usage to action_trace_v0 and updated test_extraction.cpp's expected fixtures to set both to fc::unsigned_int{0} on every action, but the make_action_trace helper that drives the actual chain::action_trace inputs was not updated. to_action_trace copied the (empty) optionals through, so the actual JSON omitted both fields while expected JSON contained "cpu_usage_us":0 and "net_usage":0 -- block_extraction/basic_single_transaction_block and block_extraction/basic_multi_transaction_block both failed the block_trace_v0 equality check. Set both fields on the chain::action_trace returned by the helper so the fixture is internally consistent. --- plugins/trace_api_plugin/test/test_extraction.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/trace_api_plugin/test/test_extraction.cpp b/plugins/trace_api_plugin/test/test_extraction.cpp index 149c7f9956..9a264c0e6c 100644 --- a/plugins/trace_api_plugin/test/test_extraction.cpp +++ b/plugins/trace_api_plugin/test/test_extraction.cpp @@ -83,7 +83,6 @@ namespace { chain::action_trace make_action_trace( uint64_t global_sequence, chain::action act, chain::name receiver ) { chain::action_trace result; - // don't think we need any information other than receiver and global sequence result.receipt.emplace(chain::action_receipt{ receiver, digest_type::hash(act), @@ -95,6 +94,11 @@ namespace { }); result.receiver = receiver; result.act = std::move(act); + // chain::action_trace::cpu_usage_us / net_usage are populated for input actions; + // the block_extraction fixtures expect these to round-trip as fc::unsigned_int{0} + // on every action, so set them here rather than at every call site. + result.cpu_usage_us = fc::unsigned_int{0}; + result.net_usage = fc::unsigned_int{0}; return result; }