Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e49318a
docs: update README with symbolication feature
alltheseas Jan 16, 2026
7bdc7dd
docs: simplify README relay limits and cleanup
alltheseas Jan 16, 2026
8b04dc8
docs(rust): add symbolication section with proper code block tags
alltheseas Jan 16, 2026
fb8de5c
feat(rust): add CHK chunking for large crash reports (Phase 2)
alltheseas Jan 16, 2026
8c39004
feat(rust): implement chunk fetching for large crash reports
alltheseas Jan 16, 2026
529ba43
docs(rust): expand rustdoc for fetch_chunks function
alltheseas Jan 16, 2026
2f9fe6b
feat(receiver): cross-relay chunk aggregation
alltheseas Jan 16, 2026
68ef61d
feat(electron): add CHK chunking for large crash reports
alltheseas Jan 16, 2026
1ae539b
feat(react-native): add CHK chunking for large crash reports
alltheseas Jan 16, 2026
eab5fa0
feat(go): add CHK chunking for large crash reports
alltheseas Jan 16, 2026
6b73a8d
feat(python): add CHK chunking for large crash reports
alltheseas Jan 16, 2026
beabb3b
feat(android,dart): add CHK chunking modules for large crash reports
alltheseas Jan 16, 2026
c052d94
feat(android,dart): integrate chunking into send flow
alltheseas Jan 16, 2026
ff356ca
feat(sdk): add 100ms delay between chunk publications
alltheseas Jan 16, 2026
ed888cd
chore(electron,react-native): update package-lock.json
alltheseas Jan 16, 2026
130d154
feat(dart,android): add relay rate limiting and progress callbacks
alltheseas Jan 16, 2026
22bb93c
feat(react-native): add relay rate limiting and progress callbacks
alltheseas Jan 16, 2026
2076b2a
feat(go,python): add relay rate limiting and progress callbacks
alltheseas Jan 16, 2026
bf8d592
feat(rust): add relay hints support for chunk fetching
alltheseas Jan 16, 2026
abb6492
feat(sdk): add publish verification and retry for chunk uploads
alltheseas Jan 16, 2026
a462bb9
fix(sdk): increase verification delay to 500ms
alltheseas Jan 16, 2026
d261088
docs: add reliability analysis for chunk uploads
alltheseas Jan 16, 2026
643375b
fix(sdk): align CHK encryption with hashtree-core across all SDKs
alltheseas Jan 16, 2026
1d94ed1
fix(rust): prevent panic on ws:// relay URLs in subscription ID
alltheseas Jan 16, 2026
2559d04
test: add CHK encryption cross-SDK test vectors
alltheseas Jan 16, 2026
6b9070d
chore: add .beads/ to gitignore
alltheseas Jan 16, 2026
f33ed4c
fix: address code review findings across all SDKs
alltheseas Jan 16, 2026
bee0184
fix: improve chunk upload error handling and fix Android pubKey deriv…
alltheseas Jan 16, 2026
f1991bb
fix: NIP-17/NIP-59 compliance fixes across all SDKs
alltheseas Jan 16, 2026
7bb5ed5
fix: add remaining NIP-17/NIP-59 receiver validations
alltheseas Jan 16, 2026
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ node_modules/
# Test vectors
test-vectors/node_modules/
test-vectors/package-lock.json

# Beads (local issue tracking)
.beads/
59 changes: 59 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,65 @@ id = sha256(json([0, pubkey, created_at, kind, tags, content]))

Returns lowercase hex string (64 characters).

## Lessons Learned

### CHK Encryption Compatibility (Critical)

**Problem**: All SDKs implemented CHK (Content Hash Key) encryption differently from the Rust reference implementation, causing complete decryption failure.

**Root Cause**: Each SDK used its own interpretation of "encrypt with content hash":
- Some used AES-256-CBC with random IV
- Others omitted HKDF key derivation
- Ciphertext format varied (IV position, tag handling)

**The Correct Algorithm** (must match `hashtree-core` exactly):

```
1. content_hash = SHA256(plaintext)
2. key = HKDF-SHA256(
ikm: content_hash,
salt: "hashtree-chk",
info: "encryption-key",
length: 32
)
3. ciphertext = AES-256-GCM(
key: key,
nonce: 12 zero bytes,
plaintext: data
)
4. output = [ciphertext][16-byte auth tag]
```

**Why each component matters**:

| Component | Purpose | If Wrong |
|-----------|---------|----------|
| HKDF | Derives encryption key from content hash | Key mismatch → decryption fails |
| Salt `"hashtree-chk"` | Domain separation | Different key → decryption fails |
| Info `"encryption-key"` | Key purpose binding | Different key → decryption fails |
| Zero nonce | Safe for CHK (same key = same content) | Different ciphertext → verification fails |
| AES-GCM | Authenticated encryption | Different algorithm → decryption fails |

**Why zero nonce is safe**: CHK is convergent encryption - the same plaintext always produces the same key. Since the key is deterministic, using a random nonce would make ciphertext non-deterministic, breaking content-addressable storage. Zero nonce is safe because the key is never reused with different content.

**Verification checklist for new implementations**:
1. Generate test vector in Rust: `cargo test chunking -- --nocapture`
2. Encrypt same plaintext in your SDK
3. Compare: content hash, derived key, ciphertext must be byte-identical
4. Decrypt Rust ciphertext in your SDK (and vice versa)

**Platform-specific libraries**:

| Platform | HKDF | AES-GCM |
|----------|------|---------|
| Rust | `hashtree-core` | (built-in) |
| Dart | `pointycastle` HKDFKeyDerivator | `pointycastle` GCMBlockCipher |
| Kotlin | Manual HMAC-SHA256 | `javax.crypto` AES/GCM/NoPadding |
| Go | `golang.org/x/crypto/hkdf` | `crypto/cipher` NewGCM |
| Python | `cryptography` HKDF | `cryptography` AESGCM |
| TypeScript (Node) | `crypto` hkdfSync | `crypto` aes-256-gcm |
| TypeScript (RN) | `@noble/hashes/hkdf` | `@noble/ciphers/aes` gcm |

## Testing

### Unit Tests
Expand Down
85 changes: 57 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,19 @@ Crash → Cache locally → App restart → Show consent dialog → User approve

All SDKs use the same default relay list, chosen for reliability:

| Relay | Max Event Size | Max WebSocket | Notes |
|-------|----------------|---------------|-------|
| `wss://relay.damus.io` | 64 KB | 128 KB | strfry defaults |
| `wss://relay.primal.net` | 64 KB | 128 KB | strfry defaults |
| `wss://nos.lol` | 128 KB | 128 KB | Fallback relay |
| Relay | Notes |
|-------|-------|
| `wss://relay.damus.io` | strfry defaults |
| `wss://relay.primal.net` | strfry defaults |
| `wss://nos.lol` | Fallback relay |

**Note:** Most relays use strfry defaults (64 KB event size, 128 KB websocket payload). The practical limit for crash reports is ~60 KB to allow for gift-wrap envelope overhead.
**Note:** Most relays enforce a 64 KB event size limit (strfry default). Keep compressed payloads under **60 KB** to allow for gift-wrap envelope overhead.

You can override these defaults via the `relays` configuration option in each SDK.

## Size Limits & Compression

Crash reports are subject to relay message size limits (see [NIP-11](https://github.com/nostr-protocol/nips/blob/master/11.md) `max_message_length`).

| Relay Limit | Compatibility |
|-------------|---------------|
| 64 KB | ~99% of relays |
| 128 KB | ~90% of relays |
| 512 KB+ | Major relays only |

**Practical limit:** Keep compressed payloads under **60 KB** for universal delivery (allows ~500 bytes for gift-wrap envelope overhead).
Crash reports are subject to relay message size limits (see [NIP-11](https://github.com/nostr-protocol/nips/blob/master/11.md) `max_message_length`). Most relays enforce a **64 KB** limit.

| Payload Size | Behavior |
|--------------|----------|
Expand Down Expand Up @@ -85,7 +77,7 @@ Gzip typically achieves **70-90% reduction** on stack traces due to their repeti
| 50 KB | ~5-10 KB | ~80-90% |
| 200 KB | ~20-40 KB | ~80-85% |

With gzip compression (70-90% reduction), most crash reports fit well within the 64 KB strfry default limit. For maximum compatibility, keep compressed payloads under 60 KB.
With gzip compression, most crash reports fit well within the 64 KB limit.

## Nostr Protocol

Expand All @@ -102,27 +94,64 @@ Per NIP-17, rumors (kind 14) must include:
- `id` - SHA256 hash of `[0, pubkey, created_at, kind, tags, content]`
- `sig: ""` - Empty string (not omitted)

Some clients (e.g., 0xchat) reject messages missing these fields.

## Shared Test Vectors

The [`test-vectors/`](test-vectors/) directory contains JSON test cases for NIP-17. All platform implementations should validate against these vectors.

## Symbolication

Release builds typically use code obfuscation/minification, producing stack traces with mangled names and memory addresses instead of readable function names and line numbers.
The Rust receiver includes built-in symbolication for 7 platforms:

| Platform | Mapping File | Notes |
|----------|--------------|-------|
| Android | `mapping.txt` | ProGuard/R8 with full line-range support |
| Electron/JS | `*.js.map` | Source map v3 |
| Flutter | `*.symbols` | Via `flutter symbolize` or direct parsing |
| Rust | Backtrace | Debug builds include source locations |
| Go | Goroutine stacks | Symbol tables usually embedded |
| Python | Tracebacks | Source file mapping |
| React Native | `*.bundle.map` | Hermes bytecode + JS source maps |

### CLI Usage

```bash
# Symbolicate a stack trace
bugstr symbolicate --platform android --input crash.txt --mappings ./mappings \
--app-id com.example.app --version 1.0.0

# Output formats: pretty (default) or json
bugstr symbolicate --platform android --input crash.txt --format json
```

### Web API

**Current status:** Symbolication tooling is not yet implemented. Crash reports contain raw stack traces as captured.
```bash
# Start server with symbolication enabled
bugstr serve --mappings ./mappings

# POST to symbolicate endpoint
curl -X POST http://localhost:3000/api/symbolicate \
-H "Content-Type: application/json" \
-d '{"platform":"android","stack_trace":"...","app_id":"com.example","version":"1.0.0"}'
```

**Planned approach:**
- Store mapping files (ProGuard, dSYM, sourcemaps) locally or in your CI
- Use platform-specific tools to symbolicate:
- **Android**: `retrace` with ProGuard mapping
- **iOS/macOS**: `atos` or `symbolicatecrash` with dSYM
- **JavaScript**: Source map support in browser devtools
- **Flutter**: `flutter symbolize` with app symbols
### Mapping File Organization

```
mappings/
android/
com.example.app/
1.0.0/mapping.txt
1.1.0/mapping.txt
electron/
my-app/
1.0.0/main.js.map
flutter/
com.example.app/
1.0.0/app.android-arm64.symbols
```
Comment on lines +140 to +152
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language specification to fenced code block.

The directory structure example should specify a language for proper syntax highlighting and accessibility.

📝 Proposed fix
-```
+```text
 mappings/
   android/
     com.example.app/
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

140-140: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@README.md` around lines 140 - 152, The fenced code block showing the
directory tree starting with "mappings/" needs a language tag for proper
highlighting and accessibility; update the triple-backtick fence that encloses
the block (the block containing lines like "mappings/", "android/",
"com.example.app/") to include a language identifier (e.g., add "text" after the
opening ```), ensuring the block is rendered with the specified language and
preserved formatting.


Contributions welcome for automated symbolication in the receiver CLI/WebUI.
The receiver automatically falls back to the newest available version if an exact version match isn't found.

## Contributing

Expand Down
Loading