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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions stellar/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

147 changes: 147 additions & 0 deletions stellar/METRICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,26 @@ pub struct WraithMetricEvent {
- `batch_send_count` - Number of batch sends
- `announce_count` - Number of announcements
- `error_count` - Number of errors
- `renew_count` - Number of name renewals (TTL extensions)
- `release_count` - Number of name releases
- `resolve_hit_count` - Number of name resolutions that found an entry
- `resolve_miss_count` - Number of name resolutions that found nothing
- `create_count` - Number of split definitions created
- `fund_count` - Number of split fundings
- `deposit_count` - Number of vault deposits
- `claim_count` - Number of vault claims
- `refund_count` - Number of vault refunds
- `proposal_count` - Number of governance proposals created
- `vote_count` - Number of governance votes cast
- `execution_count` - Number of governance proposals executed

### Gauge/Volume Metrics (absolute values)
- `send_volume` - Total volume sent (in token base units)
- `batch_size` - Size of a batch operation
- `total_transfers` - Total transfers in a batch
- `fund_volume` - Amount distributed by a split funding (in token base units)
- `deposit_volume` - Amount locked by a vault deposit (in token base units)
- `beneficiaries_per_split` - Number of beneficiaries in a split definition

### Timing Metrics (when applicable)
- `execution_time_ms` - Execution time in milliseconds
Expand All @@ -59,9 +74,12 @@ Common dimensions that can be attached to metrics:

- `scheme_id` - Stealth address scheme identifier
- `token_address` - Token contract address
- `asset_address` - Asset (token) contract address for splitter/vault metrics
- `asset_code` - Asset code (if applicable)
- `error_code` - Error code (for error metrics)
- `contract_version` - Contract version
- `proposal_id` - Governance proposal identifier
- `support` - Vote direction (`true` = for, `false` = against)

## Event Format

Expand Down Expand Up @@ -102,6 +120,135 @@ env.events().publish(
| `batch_send_volume` | After successful batch send | Total amount | `asset_address` |
| `batch_size` | After successful batch send | Number of transfers | `asset_address` |

### Wraith Names

| Metric Name | Trigger | Value | Dimensions |
|-------------|---------|-------|------------|
| `register_count` | After a name is written to storage (covers `register`, `register_on_behalf`, `bulk_register`, and auction `claim_name`) | 1 (increment) | — |
| `renew_count` | After `extend_name_ttl`, and once per `bulk_renew` batch | 1 (increment), or the batch size for `bulk_renew` | — |
| `release_count` | After a name is removed (covers `release` and `release_on_behalf`) | 1 (increment) | — |
| `resolve_hit_count` | After `resolve` finds the name entry | 1 (increment) | — |
| `resolve_miss_count` | When `resolve` does not find the name entry | 1 (increment) | — |

`bulk_register` emits one `register_count` per name (it calls the same internal
path as `register`), whereas `bulk_renew` emits a single `renew_count` carrying
the batch size as its value. Both aggregate to the same total.

`resolve` returns `NameNotFound` on a miss. The metric event is published before
the error is returned, so it is captured whenever the enclosing transaction is
applied — for example when `resolve` is invoked as a sub-call by a contract that
handles the error, or during simulation. A top-level `resolve` that fails takes
the whole transaction down with it and, like any other event in a failed
transaction, is not written to the ledger.

### Stealth Splitter

| Metric Name | Trigger | Value | Dimensions |
|-------------|---------|-------|------------|
| `create_count` | After a split definition is stored | 1 (increment) | `asset_address` |
| `beneficiaries_per_split` | After a split definition is stored | Number of beneficiaries | `asset_address` |
| `fund_count` | After a successful `fund_split` | 1 (increment) | `asset_address` |
| `fund_volume` | After a successful `fund_split` | Amount distributed | `asset_address` |

### Stealth Vault

| Metric Name | Trigger | Value | Dimensions |
|-------------|---------|-------|------------|
| `deposit_count` | After a successful `deposit` | 1 (increment) | `asset_address` |
| `deposit_volume` | After a successful `deposit` | Amount locked | `asset_address` |
| `claim_count` | After a successful `claim` | 1 (increment) | `asset_address` |
| `refund_count` | After a successful `refund` | 1 (increment) | `asset_address` |

### Governance

| Metric Name | Trigger | Value | Dimensions |
|-------------|---------|-------|------------|
| `proposal_count` | After a proposal is stored by `propose` | 1 (increment) | `proposal_id` |
| `vote_count` | After a vote is recorded by `vote` | 1 (increment) | `proposal_id`, `support` |
| `execution_count` | After a proposal is executed by `execute` | 1 (increment) | `proposal_id` |

## Emission Cost

A metric event is an ordinary contract event, so each `emit_metric` call costs
one `env.events().publish()`. Measured with `cargo bench -p wraith-stellar-bench
--bench gas`, one emission is **+4,164 instructions** and **+144 event bytes**
(with an empty dimension vector); the instruction cost is flat and does not vary
with the metric name or value.

That is negligible on a write path but material on a cheap read path:

| Op | Before | After | Delta |
|----|-------:|------:|------:|
| `wraith-names::register` (name_len=3) | 83,942 | 88,106 | +4.96% |
| `wraith-names::register` (name_len=32) | 84,490 | 88,654 | +4.93% |
| `wraith-names::resolve` (hit) | 34,962 | 39,126 | +11.91% |
| `wraith-names::resolve` (miss) | 23,559 | 27,723 | +17.67% |

`resolve` exceeds the CI bench gate's +5% per-op threshold, so
`stellar/bench/baseline.json` was rotated alongside the wiring. No other benched
op moved: the splitter, vault, and governance write paths are not in the gas
bench, and every other measured op is byte-identical.

Instrument cheap read paths deliberately — the fixed emission cost dominates
them.

## Symbol Encoding

Soroban `Symbol`s used in event topics are limited to 9 characters, so the
contract identifiers and metric names above are abbreviated on-chain. Indexers
must map the wire symbol back to the canonical name from this document.

| Contract | Wire symbol |
|----------|-------------|
| `stealth-registry` | `st_reg` |
| `stealth-sender` | `st_send` |
| `stealth-batch-sender` | `st_bat_sd` |
| `stealth-announcer` | `st_ann` |
| `wraith-names` | `wr_names` |
| `stealth-splitter` | `st_split` |
| `stealth-vault` | `st_vault` |
| `governance` | `gov` |

| Metric name | Wire symbol |
|-------------|-------------|
| `register_count` | `reg_cnt` |
| `remove_count` | `rem_cnt` |
| `lookup_count` | `lkp_cnt` |
| `send_count` | `send_cnt` |
| `send_volume` | `send_vol` |
| `batch_send_count` | `bat_send` |
| `batch_send_volume` | `bat_vol` |
| `batch_size` | `bat_size` |
| `error_count` | `err_cnt` |
| `renew_count` | `renew_cnt` |
| `release_count` | `rel_cnt` |
| `resolve_hit_count` | `res_hit` |
| `resolve_miss_count` | `res_miss` |
| `create_count` | `crt_cnt` |
| `fund_count` | `fund_cnt` |
| `fund_volume` | `fund_vol` |
| `beneficiaries_per_split` | `benef_cnt` |
| `deposit_count` | `dep_cnt` |
| `deposit_volume` | `dep_vol` |
| `claim_count` | `clm_cnt` |
| `refund_count` | `rfnd_cnt` |
| `proposal_count` | `prop_cnt` |
| `vote_count` | `vote_cnt` |
| `execution_count` | `exec_cnt` |

| Dimension name | Wire symbol |
|----------------|-------------|
| `scheme_id` | `scheme_id` |
| `token_address` | `tok_addr` |
| `asset_address` | `ast_addr` |
| `error_code` | `err_code` |
| `proposal_id` | `prop_id` |
| `support` | `support` |

The canonical constants live in `stellar/wraith-metrics/src/lib.rs`
(`contract_ids`, `metric_names`, `dimension_names`); the indexer's lookup tables
in `stellar/scripts/metrics-indexer/index.js` mirror them.

## Indexer Implementation

A reference indexer implementation is provided in `stellar/scripts/metrics-indexer/` that:
Expand Down
42 changes: 41 additions & 1 deletion stellar/SIZE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,30 @@ source/toolchain mismatch is fixed.

Historical contract Previous optimized baseline (bytes)
wraith_names 9,755

Metric emission delta (wraith_metrics wiring)
Wiring wraith_metrics::emit_metric into wraith-names, stealth-splitter,
stealth-vault, and the governance PoC adds an event publish (and the constant
Symbols it carries) to each write path, so each payload grows. Both columns
below are the optimized payload for the same contract, measured before and
after the metric calls were added; the only difference between them is the
metric emission.

Contract Before metrics (bytes) After metrics (bytes) Delta Growth
stealth_splitter 9,774 10,720 +946 +9.68%
stealth_vault 9,237 11,117 +1,880 +20.35%
governance 16,589 18,506 +1,917 +11.56%
wraith_names not measurable not measurable — —
All three measurable payloads stay far below the 112,640-byte CI budget; the
largest, governance, is 83.57% below it.

wraith_names cannot be compiled for wasm32-unknown-unknown at all (see the
note above), so its metric-emission delta cannot be measured on this toolchain.
The failure reproduces identically on the parent commit, so it is unrelated to
the metric wiring. Once the soroban-sdk bump lands and the contract builds,
re-run the command below and fill the row in; the wiring adds five call sites,
so it should land in the same +1 to +2 KB range as the other three.

Reproducing the per-contract delta
From this directory, run the same commands used by CI. Record the byte count of
each unoptimized WASM before applying the profile/optimizer, then record the
Expand All @@ -63,4 +87,20 @@ find target/wasm32-unknown-unknown/release -name '*_optimized.wasm' \
-printf '%f %s bytes\n' | sort
The optimizer is deliberately run on the release output, as the network deploys
the optimized payload rather than the intermediate compiler artifact. CI rejects
any optimized payload over 112,640 bytes.
any optimized payload over 112,640 bytes.

A workspace-wide wasm32 build fails because integration-tests pulls
soroban-sdk with the testutils feature and Cargo unifies that feature across
the whole build. To measure a single contract, name it explicitly so the
testutils-enabled members stay out of the graph:

Shell

cargo build -p stealth-vault -p stealth-splitter -p governance \
--target wasm32-unknown-unknown --release
for wasm in target/wasm32-unknown-unknown/release/*.wasm; do
stellar contract optimize --wasm "$wasm"
done

Note that stellar-cli 27.x writes <name>.optimized.wasm where the 22.0.1 CLI
pinned in CI writes <name>_optimized.wasm; match the glob to the CLI in use.
26 changes: 13 additions & 13 deletions stellar/bench/baseline.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"generated_at": "1787550854",
"commit": "4dbdc41a99fd2b11e0728d039060a530d7f25392",
"generated_at": "1787666620",
"commit": "d8e51779e61d77153b5c85adbf6ec4ca9c2aa847",
"threshold_pct": 5,
"results": [
{
Expand Down Expand Up @@ -163,44 +163,44 @@
"contract": "wraith-names",
"function": "register",
"params": "name_len=3",
"instructions": 83942,
"mem_bytes": 10503,
"instructions": 88106,
"mem_bytes": 10944,
"read_entries": 2,
"write_entries": 3,
"read_bytes": 104,
"write_bytes": 568,
"events_bytes": 204
"events_bytes": 348
},
{
"contract": "wraith-names",
"function": "register",
"params": "name_len=32",
"instructions": 84490,
"mem_bytes": 10473,
"instructions": 88654,
"mem_bytes": 10914,
"read_entries": 2,
"write_entries": 3,
"read_bytes": 104,
"write_bytes": 596,
"events_bytes": 232
"events_bytes": 376
},
{
"contract": "wraith-names",
"function": "resolve",
"params": "hit",
"instructions": 34962,
"mem_bytes": 3569,
"instructions": 39126,
"mem_bytes": 4010,
"read_entries": 2,
"write_entries": 0,
"read_bytes": 440,
"write_bytes": 0,
"events_bytes": 0
"events_bytes": 144
},
{
"contract": "wraith-names",
"function": "resolve",
"params": "miss",
"instructions": 23559,
"mem_bytes": 2214,
"instructions": 27723,
"mem_bytes": 2655,
"read_entries": 2,
"write_entries": 0,
"read_bytes": 104,
Expand Down
1 change: 1 addition & 0 deletions stellar/contracts/governance/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ crate-type = ["cdylib", "rlib"]

[dependencies]
soroban-sdk = { workspace = true }
wraith-metrics = { path = "../../wraith-metrics" }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
Loading
Loading