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
1 change: 1 addition & 0 deletions stellar/Cargo.lock

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

385 changes: 174 additions & 211 deletions stellar/ERRORS.md

Large diffs are not rendered by default.

171 changes: 64 additions & 107 deletions stellar/EVENT_INDEXING_AUDIT.md
Original file line number Diff line number Diff line change
@@ -1,139 +1,96 @@
# Stellar Event Indexing Audit (Issue #61)
Stellar Event Indexing Audit (Issue #61)
Date: 2026-06-27
Scope: All Soroban contracts in stellar/
Background: Soroban getEvents filters support 1–4 topic segments (hard limit). Unused topic slots waste filtering selectivity and force indexers to download more events than necessary.

**Date:** 2026-06-27
**Scope:** All Soroban contracts in `stellar/`
**Background:** Soroban `getEvents` filters support 1–4 topic segments (hard limit). Unused topic slots waste filtering selectivity and force indexers to download more events than necessary.
Status
Overall verdict: OPTIMAL. The two core contracts (stealth-announcer, stealth-registry) are correctly shaped. wraith-names is acceptable as-is. stealth-splitter already routes per-transfer announcements through the announcer (Issue #62 was a stale audit-doc verdict). stealth-batch-sender now does the same (Issue #63 resolved).

## Status
Findings Summary
Contract Event Current Topics Assessment Recommendation
stealth-announcer announce ("announce", scheme_id, view_tag_bucket, metadata_kind) ✅ FINAL None — v2 schema is authoritative
stealth-registry register ("register", registrant, scheme_id) ✅ OPTIMAL None
stealth-registry remove ("remove", registrant, scheme_id) ✅ OPTIMAL None
stealth-sender — No user-facing events ✅ N/A None
wraith-names register, update, release, extend (event_type, name_hash) ✅ ACCEPTABLE Optional: add owner as topic 3 (low priority)
stealth-splitter per-transfer announcement ("announce", scheme_id, view_tag_bucket, metadata_kind) via announcer ✅ OPTIMAL None — already routed through announcer (Issue #62 was a stale audit verdict)
stealth-splitter create, fund (event_type, split_id) ✅ OK None
stealth-batch-sender per-transfer announcement ("announce", scheme_id, view_tag_bucket, metadata_kind) via announcer ✅ OPTIMAL None — Issue #63 resolved
stealth-batch-sender batch summary event ("BATCH",) ✅ OK Batch-level observability; not used for recipient scan filtering
Detailed Findings
stealth-announcer
Event: announce
Topics: ("announce", scheme_id, view_tag_bucket, metadata_kind)
Data: (stealth_address, ephemeral_pub_key, metadata)

**Overall verdict: MOSTLY OPTIMAL.** The two core contracts (`stealth-announcer`, `stealth-registry`) are correctly shaped. `wraith-names` is acceptable as-is. `stealth-splitter` has a confirmed indexing deficiency in its `ANNOUNCE` events. `stealth-batch-sender` requires a follow-up review.
All 4 topic slots are used. view_tag_bucket is metadata[0] as u32, partitioning events into 256 buckets — a recipient only needs to scan ~1/256 of all announcements (~99.6% traffic reduction). metadata_kind=1 signals that metadata[0] is the view tag byte. STELLAR_V2_SCHEME_ID=2 is asserted at runtime, preventing accidental v1 data from appearing under this contract.

---
No changes needed.

## Findings Summary
stealth-registry
Events: register, remove
Topics: ("register"|"remove", registrant, scheme_id)
Data: register → stealth_meta_address; remove → ()

| Contract | Event | Current Topics | Assessment | Recommendation |
|---|---|---|---|---|
| stealth-announcer | `announce` | `("announce", scheme_id, view_tag_bucket, metadata_kind)` | ✅ FINAL | None — v2 schema is authoritative |
| stealth-registry | `register` | `("register", registrant, scheme_id)` | ✅ OPTIMAL | None |
| stealth-registry | `remove` | `("remove", registrant, scheme_id)` | ✅ OPTIMAL | None |
| stealth-sender | — | No user-facing events | ✅ N/A | None |
| wraith-names | `register`, `update`, `release`, `extend` | `(event_type, name_hash)` | ✅ ACCEPTABLE | Optional: add owner as topic 3 (low priority) |
| stealth-splitter | `ANNOUNCE` | `("ANNOUNCE",)` | ⚠️ SUBOPTIMAL | Issue #62: adopt v2 topic layout or route through announcer |
| stealth-splitter | `BATCH` | `("BATCH",)` | ⚠️ SUBOPTIMAL | Covered by Issue #62 |
| stealth-splitter | `create`, `fund` | `(event_type, split_id)` | ✅ OK | None |
| stealth-batch-sender | per-transfer event | Unknown (line 83) | ⚠️ NEEDS REVIEW | Issue #63: audit event topic layout |
| stealth-batch-sender | batch summary event | Unknown (line 95) | ⚠️ NEEDS REVIEW | Issue #63: audit event topic layout |
The dominant read pattern is "fetch meta-address for address X under scheme Y." The existing (registrant, scheme_id) topic pair maps directly to that query. A 4th topic (e.g. a hash of the meta-address) would add no useful selectivity for any realistic indexer query.

---
No changes needed.

## Detailed Findings
stealth-sender
Delegates all announcement work to the stealth-announcer contract via invoke_contract. No user-facing events are emitted directly. Internal observability events from wraith-metrics are out of scope for this audit.

### stealth-announcer
Not applicable.

**Event:** `announce`
**Topics:** `("announce", scheme_id, view_tag_bucket, metadata_kind)`
**Data:** `(stealth_address, ephemeral_pub_key, metadata)`
wraith-names
Events: register, update, release, extend
Topics: (event_type, name_hash)
Data: varies per event

All 4 topic slots are used. `view_tag_bucket` is `metadata[0] as u32`, partitioning events into 256 buckets — a recipient only needs to scan ~1/256 of all announcements (~99.6% traffic reduction). `metadata_kind=1` signals that `metadata[0]` is the view tag byte. `STELLAR_V2_SCHEME_ID=2` is asserted at runtime, preventing accidental v1 data from appearing under this contract.

**No changes needed.**

---

### stealth-registry

**Events:** `register`, `remove`
**Topics:** `("register"|"remove", registrant, scheme_id)`
**Data:** `register` → `stealth_meta_address`; `remove` → `()`

The dominant read pattern is "fetch meta-address for address X under scheme Y." The existing `(registrant, scheme_id)` topic pair maps directly to that query. A 4th topic (e.g. a hash of the meta-address) would add no useful selectivity for any realistic indexer query.

**No changes needed.**

---

### stealth-sender

Delegates all announcement work to the `stealth-announcer` contract via `invoke_contract`. No user-facing events are emitted directly. Internal observability events from `wraith-metrics` are out of scope for this audit.

**Not applicable.**

---

### wraith-names

**Events:** `register`, `update`, `release`, `extend`
**Topics:** `(event_type, name_hash)`
**Data:** varies per event

`name_hash` is `SHA-256(name)`, so `(event_type, name_hash)` is a deterministic, exact-match lookup for "history of name X" — the overwhelmingly common query. This is already optimal for the primary use case.
name_hash is SHA-256(name), so (event_type, name_hash) is a deterministic, exact-match lookup for "history of name X" — the overwhelmingly common query. This is already optimal for the primary use case.

A potential improvement is adding the owner address as topic 3, enabling queries like "all registrations by owner O." However, the owner address is not currently included in event data, so adding it would require a data-shape change. Given the low event volume of a name registry, the cost/complexity is not justified at this time.

**No changes required.** Owner-indexed queries are a low-priority optional follow-up.

---

### stealth-splitter

**Events of concern:** `ANNOUNCE`, `BATCH`

`ANNOUNCE` topics: `("ANNOUNCE",)` — only 1 topic, no filtering possible beyond event name.
`BATCH` topics: `("BATCH",)` — same problem.

A recipient scanning for stealth payments routed through the splitter must download every `ANNOUNCE` event ever emitted by the contract. This is the exact problem the v2 announcer schema was designed to solve. The correct fix is one of:

- **(a) Route through announcer:** have `stealth-splitter` call `stealth-announcer` for each transfer instead of emitting its own `ANNOUNCE`. This fixes indexing and eliminates the architectural inconsistency (splitter announcements are not currently discoverable via the same `getEvents` filter as direct announcer events).
- **(b) Adopt v2 topic layout inline:** emit `("ANNOUNCE", scheme_id, view_tag_bucket, metadata_kind)` directly, mirroring the announcer schema without the cross-contract call overhead.
No changes required. Owner-indexed queries are a low-priority optional follow-up.

Option (a) is preferred for consistency. Tracked as **Issue #62**.
stealth-splitter
Per-transfer announcements: routed through stealth-announcer via announcer_client::announce in fund_split (stealth-splitter/src/lib.rs). The resulting events are emitted by the announcer contract with the v2 topic layout ("announce", scheme_id, view_tag_bucket, metadata_kind). Recipients can apply the same topic-3 (view_tag_bucket) getEvents filter used for direct announcer traffic.

**`create` and `fund` events** (`(event_type, split_id)`) are fine — split management queries by `split_id` are the natural access pattern.
The original audit recorded splitter ANNOUNCE / BATCH as single-topic events. That verdict is stale: fund_split no longer publishes ("ANNOUNCE",) itself. Issue #62 is therefore an audit-doc correction, not a code change.

---
create and fund events ((event_type, split_id)) are fine — split management queries by split_id are the natural access pattern.

### stealth-batch-sender
stealth-batch-sender
Per-transfer announcements are routed through stealth-announcer via announcer_client::announce in batch_send (stealth-batch-sender/src/lib.rs), matching the splitter pattern. Each transfer therefore appears under the announcer with the v2 4-topic layout, including view_tag_bucket = metadata[0] as u32. Indexers no longer need a batch-sender-specific decoder for a single-topic ("ANNOUNCE",) event.

The contract emits at least two events (per-transfer at line 83, batch summary at line 95 of `src/lib.rs`). The full topic layout was not inspected during this audit. If these events follow a pattern similar to `stealth-splitter`'s `ANNOUNCE` — emitting their own announcement-style events with fewer than 4 topics and no `view_tag_bucket` — the same indexing deficiency applies.
The batch summary event ("BATCH",) with (from, count, asset) is operational observability and is not used for recipient scan filtering.

**Tracked as Issue #63** for a dedicated review.
Issue #63 is resolved.

---
v2 Schema Sign-off
The stealth-announcer v2 event schema is final and authoritative:

## v2 Schema Sign-off
text

The `stealth-announcer` v2 event schema is **final and authoritative**:

```
topics: ("announce", scheme_id: u32, view_tag_bucket: u32, metadata_kind: u32)
data: (stealth_address: Bytes, ephemeral_pub_key: Bytes, metadata: Bytes)
```

- `scheme_id = 2` (`STELLAR_V2_SCHEME_ID`) is the canonical Stellar stealth scheme identifier.
- `view_tag_bucket = metadata[0] as u32` when `metadata_kind = 1`.
- v1 events (scheme_id=1, 3-topic layout) remain readable from the old contract during any transition period (Path A migration).

SDK and off-chain indexers should treat this schema as stable. No further changes to `stealth-announcer` event structure are planned.

---

## Follow-up Issues

### Issue #62 — stealth-splitter: fix ANNOUNCE event topic layout
scheme_id = 2 (STELLAR_V2_SCHEME_ID) is the canonical Stellar stealth scheme identifier.
view_tag_bucket = metadata[0] as u32 when metadata_kind = 1.
v1 events (scheme_id=1, 3-topic layout) remain readable from the old contract during any transition period (Path A migration).
SDK and off-chain indexers should treat this schema as stable. No further changes to stealth-announcer event structure are planned.

**Problem:** `stealth-splitter` emits `ANNOUNCE` with only 1 topic, making per-recipient scan filtering impossible. Recipients must download all splitter announcements.
The reference indexer (stellar/examples/indexer/src/processor.ts) decodes this layout through a single code path shared by announcer, splitter, and batch-sender.

**Options:**
1. Route splitter per-transfer announcements through `stealth-announcer` (preferred — restores unified indexing).
2. Adopt the v2 topic layout `("ANNOUNCE", scheme_id, view_tag_bucket, metadata_kind)` inline.
Follow-up Issues
Issue #62 — stealth-splitter: fix ANNOUNCE event topic layout
Status: CLOSED (audit-doc correction). No splitter code change.

**Affects:** `ANNOUNCE` and `BATCH` events. `create`/`fund` events are unaffected.
Original problem: the audit recorded stealth-splitter as emitting ANNOUNCE with only 1 topic.

---
Correction: fund_split already routes per-transfer announcements through stealth-announcer via announcer_client::announce. Splitter output therefore already uses the full 4-topic v2 layout ("announce", scheme_id, view_tag_bucket, metadata_kind). Recipients can filter on topic-3 (view_tag_bucket) without a full scan of splitter traffic.

### Issue #63 — stealth-batch-sender: audit event topic layout
Issue #63 — stealth-batch-sender: audit event topic layout
Status: RESOLVED. Closed by the merging PR that lands this change: stpatrickghost/contracts.

**Problem:** The event topic layout in `stealth-batch-sender` was not fully reviewed in this audit. If per-transfer events lack `view_tag_bucket` and `scheme_id` topics, the same deficiency as Issue #62 applies.
Original problem: stealth-batch-sender emitted per-transfer ("ANNOUNCE",) with a single topic, so server-side topic-3 view-tag filtering could not be applied to batch-sender traffic.

**Action:** Read `stealth-batch-sender/src/lib.rs` lines 83 and 95 in full context, assess topic layout against the v2 schema, and apply the same fix as Issue #62 if needed.
Fix: batch_send now invokes the announcer contract per transfer (same announcer_client::announce pattern as the splitter). Events use ("announce", scheme_id, view_tag_bucket, metadata_kind) with view_tag_bucket = metadata[0] as u32. Covered by unit + snapshot tests and by stellar/integration-tests/tests/topic_filter.rs.
2 changes: 1 addition & 1 deletion stellar/PERF.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ ownership checks.
## Current Numbers

<!-- BENCH:CURRENT:START -->
These are the harness results auto-updated from `develop` (measured 2026-08-28, commit `ac1d79ff9a35`).
These are the harness results auto-updated from `develop` (measured 2026-08-29, commit `ff49946cbc21`).

| Contract | Function | Parameters | Instructions | Mem bytes | Read entries | Write entries | Read bytes | Write bytes | Event bytes |
|---|---|---:|---:|---:|---:|---:|---:|---:|---:|
Expand Down
8 changes: 5 additions & 3 deletions stellar/bench-crossover/src/crossover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,20 @@ fn measure_batch(n: u32) -> Measured {

let contract_id = env.register(StealthBatchSender, ());
let client = StealthBatchSenderClient::new(&env, &contract_id);
// batch_send now requires init(admin, announcer, asset_policy) (issue #155).
// batch_send now requires init(admin, announcer, asset_policy) (issue #155)
// and routes announcements through the real announcer (issue #63).
let admin = Address::generate(&env);
let announcer = Address::generate(&env);
let announcer = env.register(StealthAnnouncerContract, ());
client.init(&admin, &announcer, &None);
let (token, sender) = funded_token(&env);

let mut transfers: SorobanVec<Transfer> = vec![&env];
for i in 0..n {
transfers.push_back(Transfer {
stealth_address: Address::generate(&env),
ephemeral_pub_key: bytes(&env, 33, 0x02u8.wrapping_add(i as u8)),
ephemeral_pub_key: bytes(&env, 32, 0x02u8.wrapping_add(i as u8)),
amount: 100,
metadata: bytes(&env, 1, (i as u8).wrapping_add(1)),
});
}

Expand Down
4 changes: 3 additions & 1 deletion stellar/examples/indexer/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ NETWORK_PASSPHRASE=Test SDF Future Network ; October 2022
STEALTH_ANNOUNCER_ADDRESS=CBXYZANN0UNCERXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX0001
STEALTH_REGISTRY_ADDRESS=CBREG1STRYXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX0002
STEALTH_SENDER_ADDRESS=CBSEND3RXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX0003
STEALTH_SPLITTER_ADDRESS=CBSPL1TTERXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX0005
STEALTH_BATCH_SENDER_ADDRESS=CBBATCHS3NDXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX0006
WRAITH_NAMES_ADDRESS=CBNAM3SWRA1THXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX004

# Indexer
POLL_INTERVAL_MS=5000
PORT=3000
PORT=3000
Loading
Loading