Compare commits

...

42 Commits
v2.0.0 ... main

Author SHA1 Message Date
callebtc
4f828b5643
Merge pull request #916 from permissionlesstech/codex/fix-wear-shape-and-font-scaling
Improve Wear layouts for round screens
2026-08-26 18:19:15 +02:00
callebtc
7cf5286bfb Fix slow Wear scroll intent detection 2026-08-26 17:28:17 +02:00
callebtc
a125da48aa Polish Wear shape-aware layout implementation 2026-08-26 16:46:13 +02:00
callebtc
3ed46222ba Fade Wear chat at screen contour 2026-08-26 11:56:00 +02:00
callebtc
898cfde066 Restore Wear chat edge fading 2026-08-26 11:39:16 +02:00
callebtc
33c1ebfc60
Merge pull request #909 from Chessing234/fix/panic-wipe-destroy-db-on-failure
fix: destroy conversation database files when panic wipe throws
2026-08-25 14:37:58 +02:00
Taksh
1a9f6befd4 fix: destroy conversation database files when panic wipe throws
If SQLite clearAll fails mid-panic, in-memory state was already cleared
but a process restart could reload encrypted history from disk (#699).
Fall back to deleting the database files before reporting failure.
2026-08-25 07:40:04 +05:30
callebtc
ed617f0f1b Fix Wear chat autoscroll and focal scaling 2026-08-24 21:13:07 +02:00
callebtc
34f94882f5 Restore Wear chat content space 2026-08-24 20:57:24 +02:00
callebtc
45915fab8f Tune Wear header boundary by screen shape 2026-08-24 20:15:17 +02:00
callebtc
f744038318 Preserve width for Wear Material cards 2026-08-24 20:08:44 +02:00
callebtc
4a1de47ed6 Cover transformed Wear header boundary 2026-08-24 20:04:46 +02:00
callebtc
5d66b69129 Clip Wear messages below the chat header 2026-08-24 20:01:46 +02:00
callebtc
7788328917 Keep Wear chat rows clear of controls 2026-08-24 19:58:03 +02:00
callebtc
2e41dabd07 Add round-screen responsive Wear insets 2026-08-24 19:54:20 +02:00
callebtc
5852fca9b5 Fix Wear content clipping on watch shapes 2026-08-24 19:38:26 +02:00
callebtc
eab894ce78
Merge pull request #743 from aharshit123456/fix/geohash-signature-verification-733
fix: verify Schnorr signatures on incoming geohash Nostr events
2026-08-24 19:17:25 +02:00
GitHub Action
d55754d824 Automated update of relay data - Sun Aug 23 06:09:25 UTC 2026 2026-08-23 06:09:25 +00:00
callebtc
09b481f1ef
Merge pull request #876 from qutad/feat/announce-capability-bits
feat: Add announce capability bit assignments
2026-08-17 21:05:02 +02:00
callebtc
db97c60af1 Merge remote-tracking branch 'origin/main' into feat/announce-capability-bits 2026-08-17 20:40:43 +02:00
callebtc
2318b51630
Merge pull request #897 from permissionlesstech/codex/fix-command-processor-test-brace
Fix CommandProcessor test syntax
2026-08-17 20:38:39 +02:00
callebtc
03d1da5423 Fix CommandProcessor test syntax 2026-08-17 20:13:19 +02:00
callebtc
646044cd81 Clarify capability authentication scope 2026-08-17 19:14:05 +02:00
callebtc
35ed4417f0
Merge pull request #841 from phuctoan123/codex-fix-geohash-join-channel-routing
Fix geohash join channel routing
2026-08-17 19:09:26 +02:00
callebtc
9167013ac4
Merge pull request #863 from areebahmeddd/fix/preserve-wire-payload
feat: preserve original bytes during re-encoding of packets with foreign encoders
2026-08-17 19:08:01 +02:00
GitHub Action
b028270cc4 Automated update of relay data - Sun Aug 16 06:08:32 UTC 2026 2026-08-16 06:08:32 +00:00
wollow
22f87a493a Add announce capability bit assignments 2026-08-13 15:11:54 +03:00
callebtc
5156f7de89 Restore PRIVACY_POLICY.md 2026-08-12 01:55:05 +02:00
callebtc
93e9594bad
Merge pull request #873 from permissionlesstech/codex/bump-version-2.0.1
Prepare Android 2.0.1 release
2026-08-11 21:09:03 +02:00
callebtc
16a22316b2 Disable nondeterministic Compose group mapping 2026-08-11 20:42:26 +02:00
callebtc
c549bdb03f Bump Android release to 2.0.1 2026-08-11 20:08:57 +02:00
callebtc
c9938a36ea
Merge pull request #872 from permissionlesstech/codex/fix-reproducible-aab-mapping
Fix reproducible R8 mapping output
2026-08-11 19:57:31 +02:00
callebtc
6c5d7d1ef0 Fix reproducible R8 mapping output 2026-08-11 19:33:44 +02:00
Kane Waldo
4007bae1e2 Fix syntax error in CommandProcessorTest 2026-08-11 09:20:39 +07:00
Toan Wizard
ca992d169c
Merge branch 'main' into codex-fix-geohash-join-channel-routing 2026-08-04 09:30:27 +07:00
Kane Waldo
83eaebc357 Fix geohash join channel routing 2026-07-31 11:20:58 +07:00
areebahmeddd
4b2a9c3b0d
preserve original bytes 2026-07-31 04:59:22 +05:30
a1denvalu3
e07e3c9118 test: sign background geohash events 2026-07-30 19:57:07 +02:00
a1denvalu3
c737412f14 Merge remote-tracking branch 'upstream/main' into fix/geohash-signature-verification-733
# Conflicts:
#	app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt
2026-07-30 19:34:28 +02:00
a1denvalu3
7e22480732 fix: verify geohash signatures off main thread 2026-07-30 19:30:20 +02:00
aharshit123456
57a7d3d955 fix: verify signature before dedup in GeohashMessageHandler
Per Codex review on PR #743: the previous ordering called dedupe(event.id)
before isValidSignature(), so a forged event (bad signature, but a
content-derived id matching a legitimate event) would get its id marked
as "seen" and dropped — permanently poisoning the dedup cache. When the
genuine, validly-signed copy of the same event later arrived (e.g. via a
different relay), it was then silently discarded too, as an apparent
duplicate.

Move the signature check ahead of dedupe() so only genuinely-authenticated
event ids are ever cached, matching the ordering already used correctly in
NostrClient.handleGeohashMessage() and LocationNotesManager.handleEvent().

Adds a regression test that reproduces the exact scenario: a forged event
is rejected first, then a genuine event with the same id must still be
rendered (previously failed against the pre-fix ordering).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 14:48:27 +05:30
aharshit123456
1d80e64c69 fix: verify Schnorr signatures on incoming geohash Nostr events
Geohash chat (kind 20000) and presence (kind 20001) events, and location
notes (kind 1), were rendered/processed without ever calling
NostrEvent.isValidSignature(). A relay-side attacker (or anyone able to
publish to a connected relay) could forge an event claiming any victim's
pubkey and have it accepted as genuine in GeohashMessageHandler,
NostrClient.handleGeohashMessage, and LocationNotesManager.handleEvent.

Add a signature check at all three ingestion points, dropping any event
whose signature doesn't verify against its claimed pubkey before further
processing (dedup, PoW check, participant tracking, or UI rendering).

Adds GeohashMessageHandlerSignatureTest, which fails against the old
code (forged event accepted and rendered) and passes against the fix,
while confirming genuinely-signed events are unaffected.

Fixes #733

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 14:08:37 +05:30
29 changed files with 1380 additions and 481 deletions

156
PRIVACY_POLICY.md Normal file
View File

@ -0,0 +1,156 @@
# bitchat Privacy Policy
*Last updated: January 2025*
## Our Commitment
bitchat is designed with privacy as its foundation. We believe private communication is a fundamental human right. This policy explains how bitchat protects your privacy.
## Summary
**WE DO NOT COLLECT ANY INFORMATION.**
- **No personal data collection** - We don't collect names, emails, or phone numbers
- **No location data collection** - Location is accessed only for local processing (BLE/Geohash) and is never collected or sent to us
- **Hybrid Functionality** - bitchat offers two modes of communication:
- **Bluetooth Mesh Chat**: This mode is completely offline, using peer-to-peer Bluetooth connections. It does not use any servers or internet connection.
- **Geohash Chat**: This mode uses an internet connection to communicate with others in a specific geographic area. It relies on Nostr relays for message transport.
- **No tracking** - We have no analytics, telemetry, or user tracking
- **Open source** - You can verify these claims by reading our code
## What Information bitchat Stores
### On Your Device Only
1. **Identity Key**
- A cryptographic key generated on first launch
- Stored locally in your device's secure storage
- Allows you to maintain "favorite" relationships across app restarts
- Never leaves your device
2. **Nickname**
- The display name you choose (or auto-generated)
- Stored only on your device
- Shared with peers you communicate with
3. **Message History** (if enabled)
- When room owners enable retention, messages are saved locally
- Stored encrypted on your device
- You can delete this at any time
4. **Favorite Peers**
- Public keys of peers you mark as favorites
- Stored only on your device
- Allows you to recognize these peers in future sessions
### Temporary Session Data
During each session, bitchat temporarily maintains:
- Active peer connections (forgotten when app closes)
- Routing information for message delivery
- Cached messages for offline peers (12 hours max)
## What Information is Shared
### With Other bitchat Users
When you use bitchat, nearby peers can see:
- Your chosen nickname
- Your ephemeral public key (changes each session)
- Messages you send to public rooms or directly to them
- Your approximate Bluetooth signal strength (for connection quality)
### With Room Members
When you join a password-protected room:
- Your messages are visible to others with the password
- Your nickname appears in the member list
- Room owners can see you've joined
## What We DON'T Do
bitchat **never**:
- Collects personal information
- Collects location history
- Transmits any data to us (the developers)
- Stores data on servers
- Shares data with third parties
- Uses analytics or telemetry
- Creates user profiles
- Requires registration
## Encryption
All private messages use end-to-end encryption:
- **X25519** for key exchange
- **AES-256-GCM** for message encryption
- **Ed25519** for digital signatures
- **Argon2id** for password-protected rooms
## Your Rights
You have complete control:
- **Delete Everything**: Triple-tap the logo to instantly wipe all data
- **Leave Anytime**: Close the app and your presence disappears
- **No Account**: Nothing to delete from servers because there are none
- **Portability**: Your data never leaves your device unless you export it
## Location Data & Permissions
To provide the core functionality of bitchat, we access your device's location data. This access is necessary for the following specific purposes:
### 1. Bluetooth Low Energy (BLE) Scanning
- **Why we need it:** The Android operating system requires Location permission to scan for nearby Bluetooth LE devices (especially on Android 11 and lower). This is a system-level requirement because Bluetooth scans can theoretically be used to derive location.
- **How we use it:** We use this permission strictly to discover other bitchat peers nearby for the "Bluetooth Mesh Chat" mode.
- **Privacy protection:** We do not record or store your location during this process. The data is processed instantaneously by the Android system to facilitate the connection.
### 2. Geohash Chat Functionality
- **Why we need it:** The "Geohash Chat" mode allows you to communicate with others in your approximate geographic area.
- **How we use it:** If you enable this mode, we access your location to calculate a "geohash" (a short alphanumeric string representing a geographic region). This geohash is used to find and subscribe to relevant channels on decentralized Nostr relays.
- **Privacy protection:**
- Your precise GPS coordinates are **never** sent to any server or peer.
- Only the coarse geohash (representing an area, not a pinpoint) is shared with the Nostr network.
- You can use the "Bluetooth Mesh Chat" mode without this feature if you prefer.
**We do not collect, store, or share your location history.** Location data is processed locally on your device to enable these specific features.
## Children's Privacy
bitchat does not knowingly collect information from children. The app has no age verification because it collects no personal information from anyone.
## Data Retention
- **Messages**: Deleted from memory when app closes (unless room retention is enabled)
- **Identity Key**: Persists until you delete the app
- **Favorites**: Persist until you remove them or delete the app
- **Everything Else**: Exists only during active sessions
## Security Measures
- All communication is encrypted
- No data transmitted to servers (there are none)
- Open source code for public audit
- Regular security updates
- Cryptographic signatures prevent tampering
## Changes to This Policy
If we update this policy:
- The "Last updated" date will change
- The updated policy will be included in the app
- No retroactive changes can affect data (since we don't collect any)
## Contact
bitchat is an open source project. For privacy questions:
- Review our code: https://github.com/yourusername/bitchat
- Open an issue on GitHub
- Join the discussion in public rooms
## Philosophy
Privacy isn't just a feature—it's the entire point. bitchat proves that modern communication doesn't require surrendering your privacy. No accounts, no servers, no surveillance. Just people talking freely.
---
*This policy is released into the public domain under The Unlicense, just like bitchat itself.*

View File

@ -30,8 +30,8 @@ android {
applicationId = "com.bitchat.droid" applicationId = "com.bitchat.droid"
minSdk = libs.versions.minSdk.get().toInt() minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = 37 versionCode = 38
versionName = "2.0.0" versionName = "2.0.1"
buildConfigField( buildConfigField(
"String", "String",
"GITHUB_RELEASE_CERT_SHA256", "GITHUB_RELEASE_CERT_SHA256",
@ -111,6 +111,13 @@ android {
} }
} }
composeCompiler {
// Kotlin 2.4.10's optional Compose group-key mapping depends on unspecified
// class-file iteration order. Keep the normal R8 mapping, but omit that
// augmentation until its producer is deterministic across clean builds.
includeComposeMappingFile.set(false)
}
kotlin { kotlin {
jvmToolchain(21) jvmToolchain(21)
compilerOptions { compilerOptions {

View File

@ -1,414 +1,400 @@
Relay URL,Latitude,Longitude Relay URL,Latitude,Longitude
nostr.n7ekb.net,47.4941,-122.294
relay.kilombino.com,43.6532,-79.3832
relay02.lnfi.network,35.6764,139.65
relay.liberbitworld.org,43.6532,-79.3832
articles.layer3.news,37.3387,-121.885
relay.nostr.net,43.6532,-79.3832
nostr.easycryptosend.it,43.6532,-79.3832
strfry.bonsai.com,39.0438,-77.4874
relay.olas.app:443,60.1699,24.9384
relay.sharegap.net,43.6532,-79.3832
relay.ditto.pub,43.6532,-79.3832
nostrride.io,37.3986,-121.964
relay.favillakey.io,-27.4705,153.026
nostr-relay.xbytez.io,50.6924,3.20113
relay.nostrmap.net:443,60.1699,24.9384
mostro-p2p.tech,50.1109,8.68213
relay.aidatanorge.no,43.6532,-79.3832
nostr.openhoofd.nl,51.5717,3.70417
relay.chatbett.de,40.7128,-74.006
relay.ohstr.com:443,43.6532,-79.3832
nostr.overmind.lol:443,43.6532,-79.3832
nostr.red5d.dev,43.6532,-79.3832
relay.snort.social,53.3498,-6.26031
relay.directsponsor.net,42.8864,-78.8784
relay.bullishbounty.com:443,43.6532,-79.3832
relay.scuba323.com,40.8218,-74.45
nostr.spicyz.io,43.6532,-79.3832
nostr.chaima.info,50.1109,8.68213
nostr.purpura.cloud,43.6532,-79.3832
relay.nostrops.com,32.71,-96.6745
relay.veganostr.com:443,60.1699,24.9384
relay.ohstr.com,43.6532,-79.3832
soloco.nl,43.6532,-79.3832
nostr.plantroon.com:443,50.1013,8.62643
relay.chorus.community:443,48.5333,10.7
relay.fiatdenier.com,43.7787,-79.5393
nostr.whitenode45.ddns.net,40.55,-74.4758
relay.cosmicbolt.net:443,37.3986,-121.964
relay.lanacoin-eternity.com:443,40.8302,-74.1299
relay.cosmicbolt.net,37.3986,-121.964
fanfares.nostr1.com:443,40.7057,-74.0136
nostr-kyomu-haskell.onrender.com,37.7775,-122.397
relay.keykeeper.world,40.7824,-74.0711
relay-testnet.k8s.layer3.news,37.3387,-121.885
dm-test-strfry-generic.samt.st,43.6532,-79.3832
relay.laantungir.net,-19.4692,-42.5315
nostr.sathoarder.com,48.5734,7.75211
relay.mccormick.cx:443,52.3563,4.95714
relay.solife.me,43.6532,-79.3832
relay.fountain.fm:443,43.6532,-79.3832
nostrcity-club.fly.dev,38.7946,-106.535
nostr.mas-family.eu,60.3478,15.7505
no.str.cr,8.96171,-83.5246
relay.wavefunc.live,41.8781,-87.6298
relay.cyberguy.fyi,52.6907,4.8181
adre.su,59.9311,30.3609
relay.erybody.com,41.4513,-81.7021
relay.mrmave.work,43.6532,-79.3832
relay.decentralia.fr,48.122,11.589
relay.mccormick.cx,52.3563,4.95714
nostr.dlcdevkit.com:443,40.0992,-83.1141
nostr-02.yakihonne.com,1.32123,103.695
nostr.21crypto.ch,47.5356,8.73209
slick.mjex.me,39.0418,-77.4744
nostr.spicyz.io:443,43.6532,-79.3832
relay-dev.gulugulu.moe,43.6532,-79.3832
relay.nostx.io,43.6532,-79.3832
relay.wisp.talk,49.4543,11.0746
relay.plebeian.market:443,50.1109,8.68213
relay.mostr.pub:443,43.6532,-79.3832
relay.aarpia.com,37.3986,-121.964
chat-relay.zap-work.com,43.6532,-79.3832
chat-relay.zap-work.com:443,43.6532,-79.3832
relay.mitchelltribe.com:443,39.0438,-77.4874
relay.paulstephenborile.com,49.4543,11.0746
relay.trotters.cc,43.6532,-79.3832
nexus.libernet.app:443,43.6532,-79.3832
syb.lol,34.0549,-118.243
relay.cypherflow.ai,48.8575,2.35138
relay.despera.space,52.1782,20.9926
relay.loveisbitcoin.com,43.6532,-79.3832
relay.rushmi0.win,43.6532,-79.3832
relay.degmods.com,50.4754,12.3683
relay.satlantis.io,39.0438,-77.4874
relay.underorion.se,50.1109,8.68213
nostr.mom:443,50.4754,12.3683
nostr-verified.wellorder.net,45.5201,-122.99
relay.nostreon.com,60.1699,24.9384
nostr.christiansass.de,54.3213,10.1349
rilo.nostria.app:443,43.6532,-79.3832
relay.trotters.cc:443,43.6532,-79.3832
kotukonostr.onrender.com,37.7775,-122.397
relay.nmail.li,50.9871,2.12554
relay.artio.inf.unibe.ch,46.9501,7.43678
nos.lol,50.4754,12.3683
nostr.snowbla.de:443,50.4754,12.3683
relay.openresist.com,43.6532,-79.3832
ribo.us.nostria.app,43.6532,-79.3832
bitchat.nostr1.com,40.7057,-74.0136
vm-1734.lnvps.cloud,53.3498,-6.26031
nostr-relay.xbytez.io:443,50.6924,3.20113
nostr.sathoarder.com:443,48.5734,7.75211
bcast.girino.org,43.6532,-79.3832
ribo.us.nostria.app:443,43.6532,-79.3832
strfry.shock.network,39.0438,-77.4874
relay.staging.plebeian.market:443,51.5072,-0.127586
offchain.pub:443,39.1585,-94.5728
relayone.soundhsa.com,39.0997,-94.5786
relay.libernet.app,43.6532,-79.3832
relay.earthly.city,34.1749,-118.54
relayrs.notoshi.win:443,43.6532,-79.3832
nostr.vulpem.com,49.4543,11.0746
relay.getsafebox.app:443,43.6532,-79.3832
relay.paulstephenborile.com:443,49.4543,11.0746
nostr.unkn0wn.world,46.8499,9.53287
relay.binaryrobot.com,43.6532,-79.3832
relay.wellorder.net,45.5201,-122.99
relayone.geektank.ai:443,39.0997,-94.5786
relay.wasabiwallet.io,43.6532,-79.3832
top.testrelay.top,43.6532,-79.3832
nostr.2b9t.xyz,34.0549,-118.243
relay.nostr.com,50.1109,8.68213
nostr.azzamo.net:443,52.2633,21.0283
nostr.dlcdevkit.com,40.0992,-83.1141
nostr.robosats.org,64.1476,-21.9392
nostr.iskarion.ddns.net,43.3076,-2.95421
relay2.angor.io,48.1046,11.6002
relay.mmwaves.de,48.8575,2.35138
relay.agorist.space:443,52.3734,4.89406
relay.dreamith.to,43.6532,-79.3832
nostr-02.uid.ovh,50.9871,2.12554
nostr.thebiglake.org,32.71,-96.6745
nostr-relay.corb.net,39.6478,-104.988
relay.dyne.org,49.0291,8.35705
nosflare-leefcore.leefcore.workers.dev,43.6532,-79.3832
relay.angor.io,48.1046,11.6002
nostrelay.circum.space,52.2245,8.826
nostr-01.uid.ovh,50.9871,2.12554
maxq.descendant.io,43.6532,-79.3832
offchain.bostr.online,43.6532,-79.3832
relay.binaryrobot.com:443,43.6532,-79.3832
nostr.planix.org,43.6532,-79.3832
tribune-panel-growing-noon.trycloudflare.com,43.6532,-79.3832
prl.plus,55.7628,37.5983
relay.mwaters.net,50.9871,2.12554
dev.relay.stream,43.6532,-79.3832
relay.bornheimer.app,50.1109,8.68213
inbox.scuba323.com,40.8218,-74.45
relay-us.zombi.cloudrodion.com,40.7862,-74.0743
relay.nuts.cash,52.3676,4.90414
bruh.samt.st,43.6532,-79.3832
nostr.carroarmato0.be:443,50.914,3.21378
relay.plebeian.market,50.1109,8.68213
schnorr.me:443,43.6532,-79.3832
nostr.thalheim.io:443,60.1699,24.9384
nostr.88mph.life,52.1941,-2.21905
cache.trustr.ing,43.6548,-79.3885
ec2.f7z.io,60.1699,24.9384
relay.btcforplebs.com,43.6532,-79.3832
nostr.liberty.fans,36.8767,-89.5879
syb.lol:443,34.0549,-118.243
nostr.islandarea.net:443,35.4669,-97.6473
nostr.mom,50.4754,12.3683
nostr.data.haus:443,50.4754,12.3683
strfry.bonsai.com:443,39.0438,-77.4874
nostr.highway15.net,28.0445,-82.6699
relay.wavlake.com:443,41.2619,-95.8608
relay.nostriot.com:443,41.5695,-83.9786
relay.gulugulu.moe:443,43.6532,-79.3832
rele.speyhard.fi,50.1109,8.68213
nostr.hoppe-relay.it.com,42.8864,-78.8784
nostr2.girino.org:443,43.6532,-79.3832
nostr.oxtr.dev,50.4754,12.3683
nostr.bitcoiner.social:443,40.7608,-111.891
nostrrelay.taylorperron.com,45.5029,-73.5723
nostr-01.yakihonne.com:443,1.32123,103.695
nostr.tagomago.me,42.3601,-71.0589
relay.hivetalk.org,40.8302,-74.1299
relay.sigit.io:443,50.4754,12.3683
relay.openresist.com:443,43.6532,-79.3832
cs-relay.nostrdev.com:443,50.4754,12.3683
no.str.cr:443,8.96171,-83.5246
relay.nearhood.co.uk,51.5134,-0.0890675
relay.lanavault.space,60.1699,24.9384
relay.guggero.org,46.5971,9.59652
nexus.libernet.app,43.6532,-79.3832
relay.nostriot.com,41.5695,-83.9786
dm-test-strfry-discovery.samt.st:443,43.6532,-79.3832
x.kojira.io:443,43.6532,-79.3832
nostr.4rs.nl,49.0291,8.35696
relayone.geektank.ai,39.0997,-94.5786
nostr.debate.report,50.1109,8.68213
relay.gulugulu.moe,43.6532,-79.3832
public.crostr.com,43.6532,-79.3832
offchain.pub,39.1585,-94.5728
relay.tdw.lol,41.8781,-87.6298
relay5.bitransfer.org,43.6532,-79.3832
relay.manneken.brussels,49.4543,11.0746
chorus.pjv.me,45.5201,-122.99
relay.fckstate.net,59.3293,18.0686
treuzkas.branruz.com,48.8575,2.35138
nostr-relay.cbrx.io,43.6532,-79.3832
nostr-relay.nextblockvending.com,47.2343,-119.853
nostr.fullstackcash.net,45.5201,-122.99
relay.primal.net,43.6532,-79.3832
relay.mappingbitcoin.com,43.6532,-79.3832
nostr.mikoshi.de,50.1109,8.68213
nostr.stakey.net,52.3676,4.90414
nostr-rs-relay-qj1h.onrender.com,37.7775,-122.397
relay.wavlake.com,41.2619,-95.8608
relay.ordoplay.com,50.1109,8.68213
relay.klabo.world,47.674,-122.122
buzz.cashu.space,50.1109,8.68213
relay.nostrhub.fr,48.1045,11.6004
portal-relay.pareto.space,49.0291,8.35696
dev-relay.nostreon.com,60.1699,24.9384
relay.islandbitcoin.com,12.8498,77.6545
testr.nymble.world,40.8054,-74.0241
relayrs.notoshi.win,43.6532,-79.3832
nostr-relay.corb.net:443,39.6478,-104.988
cdn.satellite.earth,40.8302,-74.1299
rilo.nostria.app,43.6532,-79.3832
nostr.pbfs.io,50.4754,12.3683
nostr2.girino.org,43.6532,-79.3832
0x-nostr-relay.fly.dev,38.7946,-106.535
nostr.girino.org,43.6532,-79.3832
relay.layer.systems,49.0291,8.35695
relay.olas.app,60.1699,24.9384
ribo.eu.nostria.app,43.6532,-79.3832
buzz.ac2n-share.kozow.com,45.764,4.83566
auth.nostr1.com,40.7057,-74.0136
relay.bitmacro.cloud,43.6532,-79.3832
relay.qstr.app,50.1109,8.68213
relay.ru.ac.th,13.7607,100.627
ribo.nostria.app:443,43.6532,-79.3832
relay.44billion.net,43.6532,-79.3832
cs-relay.nostrdev.com,50.4754,12.3683
relay.nostrian-conquest.com:443,41.223,-111.974
relay.bullishbounty.com,43.6532,-79.3832
relay.edufeed.org,49.4521,11.0767
porchlight.social,43.6532,-79.3832
nostr.bitcoiner.social,40.7608,-111.891
testnet-relay.samt.st,40.8302,-74.1299
relay.vrtmrz.net,43.6532,-79.3832
nostr.wecsats.io:443,43.6532,-79.3832
relay.satmaxt.xyz:443,43.6532,-79.3832
nostr.snowbla.de,50.4754,12.3683
nostr.rtvslawenia.com:443,49.4543,11.0746
relay.flashapp.me,43.6548,-79.3885
purplerelay.com:443,43.6532,-79.3832
relay.staging.plebeian.market,51.5072,-0.127586
relay.libernet.app:443,43.6532,-79.3832
relay.0xchat.com,43.6532,-79.3832
myvoiceourstory.org,37.3598,-121.981 myvoiceourstory.org,37.3598,-121.981
nrs-01.darkcloudarcade.com:443,39.0997,-94.5786 nostr.linky.fit,50.1109,8.68213
relay.vrtmrz.net:443,43.6532,-79.3832
relay.zone667.com,60.1699,24.9384
nostr.nodesmap.com,59.3327,18.0656
relay.agorist.space,52.3734,4.89406
relayone.soundhsa.com:443,39.0997,-94.5786
relay.satmaxt.xyz,43.6532,-79.3832
relay.zapanimations.com,40.8054,-74.0241
relay.sovereignresonance.org,48.8575,2.35138
nostr.relay.hedwig.sh,60.1699,24.9384
relay-dev.gulugulu.moe:443,43.6532,-79.3832
relay.arx-ccn.com,50.4754,12.3683
nip85.nosfabrica.com,39.0997,-94.5786
nrs-01.darkcloudarcade.com,39.0997,-94.5786
spamspamspamspam.rest,43.6532,-79.3832
nostr.davenov.com,50.1109,8.68213
relay.littlebitstudios.com,43.6532,-79.3832
purplerelay.com,43.6532,-79.3832
nos.lol:443,50.4754,12.3683
relay2.veganostr.com,60.1699,24.9384
relay-rpi.edufeed.org,49.4521,11.0767
relay.lacrypta.ar,43.6532,-79.3832
relay.tdw.gg,34.0549,-118.243
relay.getvia.xyz,60.1699,24.9384
relay.lanacoin-eternity.com,40.8302,-74.1299
conduitl2.fly.dev,38.7946,-106.535
nostr.rtvslawenia.com,49.4543,11.0746
relay.wavefunc.live:443,41.8781,-87.6298
nostr.carroarmato0.be,50.914,3.21378
nostr.pbfs.io:443,50.4754,12.3683
ribo.nostria.app,43.6532,-79.3832
nostr.sovereignservices.xyz,43.6532,-79.3832
relay.mostr.pub,43.6532,-79.3832
schnorr.me,43.6532,-79.3832
x.kojira.io,43.6532,-79.3832
public.obelisk.ar,43.6532,-79.3832 public.obelisk.ar,43.6532,-79.3832
relay.angor.io:443,48.1046,11.6002 relay.i9.eti.br,43.6532,-79.3832
nostr.oxtr.dev:443,50.4754,12.3683
nostr.ltd,19.076,72.8777
nostr.tac.lol:443,47.4748,-122.273
nostr.twinkle.lol,51.902,7.6657
relay.s-w.art,43.6532,-79.3832
relay.openfarmtools.org,60.1699,24.9384
nostr.hekster.org,37.3986,-121.964
strfry.apps3.slidestr.net,40.4167,-3.70329
relay.mypathtofire.de,42.8864,-78.8784
nostr.wecsats.io,43.6532,-79.3832
nostr.computingcache.com,34.0356,-118.442
nostr.janx.com,43.6532,-79.3832
bread.nostrsms.com,40.8218,-74.45
relay.chorus.community,48.5333,10.7
testnet-relay.samt.st:443,40.8302,-74.1299
relay.kaleidoswap.com,50.8476,4.35717
yabu.me,35.6092,139.73 yabu.me,35.6092,139.73
nostr-dev.wellorder.net,45.5201,-122.99 relay-us.zombi.cloudrodion.com,40.7862,-74.0743
relay.layer.systems:443,49.0291,8.35695
relay-can.zombi.cloudrodion.com,43.6532,-79.3832
nostr-relay.zimage.com,34.282,-118.439
relay.fundstr.me,42.3601,-71.0589
nostr.stakey.net:443,52.3676,4.90414
relay.satsmarkt.club,52.6907,4.8181
relay.satsapp.me,50.4754,12.3683
bendernostur.duckdns.org:8443,50.1109,8.68213
relay.illuminodes.com,43.6532,-79.3832
nostr.chaima.info:443,50.1109,8.68213
nostr.hekster.org:443,37.3986,-121.964
nostr.yutakobayashi.com,43.6532,-79.3832
freelay.sovbit.host,60.1699,24.9384
insta-relay.apps3.slidestr.net,40.4167,-3.70329
infinity-signal-relay.digitalforlifeagency.workers.dev,43.6532,-79.3832
relay.internationalright-wing.org,-22.4692,-48.9875
relay.mmwaves.de:443,48.8575,2.35138
public.crostr.com:443,43.6532,-79.3832
relay.nostrian-conquest.com,41.223,-111.974
relay-rpi.edufeed.org:443,49.4521,11.0767
relay2.angor.io:443,48.1046,11.6002
relay-fra.zombi.cloudrodion.com,48.8566,2.35222
bunk-test.feeds.relay.tools,38.6327,-90.1961
nostr-pr02.redscrypt.org,52.3676,4.90414
nostr.tac.lol,47.4748,-122.273
nostr.overpay.com,29.7449,-95.5343
relay.novospes.com,43.6532,-79.3832
nostrcity-club.fly.dev:443,38.7946,-106.535
relay01.lnfi.network,35.6764,139.65
relay.lightning.pub:443,39.0438,-77.4874
relay.pyramid.li,47.4093,8.46503
relay.nostu.be,40.4167,-3.70329 relay.nostu.be,40.4167,-3.70329
nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874 relay.favillakey.io,-27.4705,153.026
relay.endfiat.money:443,59.3327,18.0656 nostr.mikoshi.de,50.1109,8.68213
nostr.spaceshell.xyz,43.6532,-79.3832 relay.agilesolutionlabs.com,43.6532,-79.3832
21milionidinostr.duckdns.org,45.5192,9.08625 nostr.sovereignservices.xyz,43.6532,-79.3832
relay.qstr.app,50.1109,8.68213
articles.layer3.news:443,37.3387,-121.885 articles.layer3.news:443,37.3387,-121.885
wheat.happytavern.co,43.6532,-79.3832 relay.nostrops.com,32.71,-96.6745
nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874 relay.mrmave.work,43.6532,-79.3832
nostr.na.social,43.6532,-79.3832 nostr.islandarea.net:443,35.4669,-97.6473
social.amanah.eblessing.co,48.1046,11.6002 nostr.azzamo.net:443,52.2633,21.0283
reraw.pbla2fish.cc,43.6532,-79.3832 ec2.f7z.io,60.1699,24.9384
nostr.girino.org:443,43.6532,-79.3832
staging.yabu.me,35.6092,139.73
nostr.islandarea.net,35.4669,-97.6473
relay.endfiat.money,59.3327,18.0656
relay.mitchelltribe.com,39.0438,-77.4874
nostr.myshosholoza.co.za,52.3676,4.90414
relay.staging.commonshub.brussels,49.4543,11.0746
bridge.tagomago.me,42.3601,-71.0589
relay.agentry.com,42.8864,-78.8784
testnet.samt.st,43.6532,-79.3832
nostr.wild-vibes.ts.net,48.8566,2.35222
relay.getsafebox.app,43.6532,-79.3832
relay.notoshi.win,13.3396,100.93
bitcoinostr.duckdns.org,41.1976,1.11167
relay.sigit.io,50.4754,12.3683
bucket.coracle.social,37.7775,-122.397
dm-test-strfry-discovery.samt.st,43.6532,-79.3832
vault.iris.to:443,43.6532,-79.3832
relay.wisp.talk:443,49.4543,11.0746
relay.mostro.network,40.8302,-74.1299
nostr.l484.com,30.2672,-97.7431
relay.froth.zone,60.1699,24.9384
relay.beamhop.com,43.6532,-79.3832
relay.sincensura.org,43.6532,-79.3832
relay.ditto.pub:443,43.6532,-79.3832
nostr.azzamo.net,52.2633,21.0283
relay.edufeed.org:443,49.4521,11.0767
ribo.eu.nostria.app:443,43.6532,-79.3832
vault.iris.to,43.6532,-79.3832
nostr.plantroon.com,50.1013,8.62643
fanfares.nostr1.com,40.7057,-74.0136
budabit.nostr1.com,40.7057,-74.0136
relay.damus.io,43.6532,-79.3832
strfry.shock.network:443,39.0438,-77.4874
bitcoiner.social,40.7608,-111.891
basspistol.org,49.0291,8.35696
nostr.thalheim.io,60.1699,24.9384
relay.conduit.market,38.7946,-106.535
relay.yoinekodo.jp,43.6532,-79.3832
nostrbtc.com,43.6532,-79.3832
relay.lightning.pub,39.0438,-77.4874
nostr-01.yakihonne.com,1.32123,103.695
nostr-pub.wellorder.net,45.5201,-122.99
strfry.ymir.cloud,43.6532,-79.3832
relay.minibolt.info,43.6532,-79.3832
nostr.data.haus,50.4754,12.3683
relay.otrta.me,50.1109,8.68213
relay.samt.st,40.8302,-74.1299
relay.nostr.blockhenge.com,39.0438,-77.4874
relay.minibolt.info:443,43.6532,-79.3832
nostr.2b9t.xyz:443,34.0549,-118.243
dm-test-nostr-rs-42-disabled.samt.st,43.6532,-79.3832
relay.veganostr.com,60.1699,24.9384 relay.veganostr.com,60.1699,24.9384
relay.fountain.fm,43.6532,-79.3832 nostr.dlcdevkit.com,40.0992,-83.1141
node.kommonzenze.de,49.4521,11.0767 relay.44billion.net,43.6532,-79.3832
relay-arg.zombi.cloudrodion.com,1.35208,103.82 bendernostur.duckdns.org:8443,50.1109,8.68213
nostr.myshosholoza.co.za:443,52.3676,4.90414 nostras.app,38.7946,-106.535
temp.iris.to,43.6532,-79.3832 cs-relay.nostrdev.com:443,50.4754,12.3683
nostr.stakey.net,52.3676,4.90414
support.flotilla.social,32.9483,-96.7299
relay.ordoplay.com,50.1109,8.68213
nostr.stakey.net:443,52.3676,4.90414
relay.nostrmap.net,60.1699,24.9384 relay.nostrmap.net,60.1699,24.9384
relayone.geektank.ai,39.0997,-94.5786
relay.novospes.com,43.6532,-79.3832
relay.dyne.org,49.0291,8.35705
nostrrelay.taylorperron.com,45.5029,-73.5723
nostrbtc.com,43.6532,-79.3832
relay.lanavault.space,60.1699,24.9384
relay.bullishbounty.com:443,43.6532,-79.3832
relay.layer.systems:443,49.0291,8.35695
vault.iris.to:443,43.6532,-79.3832
relay.getsafebox.app:443,43.6532,-79.3832
relay.wellorder.net,45.5201,-122.99
relay.scuba323.com,40.8218,-74.45
dm-test-strfry-discovery.samt.st,43.6532,-79.3832
temp.iris.to,43.6532,-79.3832
relay.paulstephenborile.com,49.4543,11.0746
relay.cyberguy.fyi,52.6907,4.8181
nostr.wecsats.io:443,43.6532,-79.3832
bread.nostrsms.com,40.8218,-74.45
relay.plebeian.market,50.1109,8.68213
relay.laantungir.net,-19.4692,-42.5315
nostr.overmind.lol,43.6532,-79.3832
nip85.nosfabrica.com,39.0997,-94.5786
relay.conduit.market,38.7946,-106.535
relay.sigit.io,50.4754,12.3683
relay.dreamith.to,43.6532,-79.3832
relay.decentralia.fr,48.122,11.589
nexus.libernet.app,43.6532,-79.3832
relay.nmail.li,50.9871,2.12554
nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874
nostr-relay.nextblockvending.com,47.2343,-119.853
nostr-relay.corb.net,39.6478,-104.988
relay.agentry.com,42.8864,-78.8784
budabit.nostr1.com,40.7057,-74.0136
relay.cypherflow.ai,48.8575,2.35138
purplerelay.com,43.6532,-79.3832
nostr.islandarea.net,35.4669,-97.6473
relay.edufeed.org,49.4521,11.0767
relay.bnos.space,43.6532,-79.3832
relay.kizuna-miki.com,35.694,139.754
nostr.snowbla.de,50.4754,12.3683
bitchat.nostr1.com,40.7057,-74.0136
relay.beamhop.com,43.6532,-79.3832
relay.wavefunc.live,41.8781,-87.6298
relayrs.notoshi.win,43.6532,-79.3832
relay.nostrhub.fr,48.1045,11.6004
relay01.lnfi.network,35.6764,139.65
relay.bitos.space,43.6532,-79.3832
relay.noeudlibre.fr,50.6924,3.20113
relay.orly.dev,32.7767,-96.797 relay.orly.dev,32.7767,-96.797
nostrelay.circum.space:443,52.2245,8.826 relay.paulstephenborile.com:443,49.4543,11.0746
relay.atsocy.com,43.6532,-79.3832
relay.keykeeper.world,40.7824,-74.0711
nostrmxn.lulus.com.mx,59.4016,17.9455
maxq.descendant.io,43.6532,-79.3832
nostr.spicyz.io:443,43.6532,-79.3832
nostr.chaima.info:443,50.1109,8.68213
relay.opmaat.org,60.1699,24.9384
nostr.notribe.net,40.8302,-74.1299 nostr.notribe.net,40.8302,-74.1299
nostr-01.yakihonne.com:443,1.32123,103.695
offchain.pub,39.1585,-94.5728
relay.agora.social,50.7383,15.0648
relay.tdw.lol,41.8781,-87.6298
nostr.nodesmap.com,59.3327,18.0656
relay.getvia.xyz,60.1699,24.9384
public.crostr.com,43.6532,-79.3832
relay.agorist.space,52.3734,4.89406
relay.lacrypta.ar,43.6532,-79.3832
nostr.spaceshell.xyz,43.6532,-79.3832
relay.nuts.cash,52.3676,4.90414
relay.otrta.me,50.1109,8.68213
relay.satmaxt.xyz,43.6532,-79.3832
relay.earthly.city,34.1749,-118.54
relay.wisp.talk:443,49.4543,11.0746
mostro-p2p.tech,50.1109,8.68213
nostr.thalheim.io,60.1699,24.9384
relay.s-w.art,43.6532,-79.3832
nostr.chaima.info,50.1109,8.68213
articles.layer3.news,37.3387,-121.885
nostr2.girino.org:443,43.6532,-79.3832
relay.mostr.pub:443,43.6532,-79.3832
relay.ditto.pub:443,43.6532,-79.3832
relay.manneken.brussels,49.4543,11.0746
relay.kaleidoswap.com,50.8476,4.35717
nostrelay.circum.space,52.2245,8.826
relay.beginningend.com,35.2227,-97.4786
relay-dev.gulugulu.moe,43.6532,-79.3832
relay.fundstr.me,42.3601,-71.0589
relay.jmoose.rocks:443,37.4419,-122.143
relay.nostr.dev.br,48.1046,11.6002
relay.minibolt.info,43.6532,-79.3832
nostr.unkn0wn.world,46.8499,9.53287
vault.iris.to,43.6532,-79.3832
relay.guggero.org,46.5971,9.59652
relay.getsafebox.app,43.6532,-79.3832
relay.bitmacro.cloud,43.6532,-79.3832
relay.wavlake.com,41.2619,-95.8608
nostr.hekster.org:443,37.3986,-121.964
relay.lightning.pub,39.0438,-77.4874
relay-dev.gulugulu.moe:443,43.6532,-79.3832
soloco.nl,43.6532,-79.3832
nostr.hifish.org,47.4009,8.57814
relay-can.zombi.cloudrodion.com,43.6532,-79.3832
dm-test-strfry-generic.samt.st,43.6532,-79.3832
relay.wavefunc.live:443,41.8781,-87.6298
porchlight.social,43.6532,-79.3832
relay.zone667.com,60.1699,24.9384
nostr.data.haus:443,50.4754,12.3683
relay.nexterz.com,43.6532,-79.3832
relay.sigit.io:443,50.4754,12.3683
nostr.girino.org,43.6532,-79.3832
nostr.liberty.fans,36.8767,-89.5879
wheat.happytavern.co,43.6532,-79.3832
relay.0xchat.com,43.6532,-79.3832
inbox.scuba323.com,40.8218,-74.45
nostr.vulpem.com,49.4543,11.0746
relay.damustr.com,43.6532,-79.3832
no.str.cr:443,8.96171,-83.5246
relay.vrtmrz.net:443,43.6532,-79.3832
vm-1734.lnvps.cloud,53.3498,-6.26031
testnet.samt.st,43.6532,-79.3832
nostr.rtvslawenia.com:443,49.4543,11.0746
nostr.sathoarder.com:443,48.5734,7.75211
relay.cosmicbolt.net,37.3986,-121.964
relay.staging.plebeian.market,51.5072,-0.127586
relayone.soundhsa.com,39.0997,-94.5786
relay.olas.app:443,60.1699,24.9384
relay.illuminodes.com,43.6532,-79.3832
relay.chorus.community:443,48.5333,10.7
relay.ohstr.com:443,43.6532,-79.3832
fanfares.nostr1.com:443,40.7057,-74.0136
nostr.planix.org,43.6532,-79.3832
schnorr.me,43.6532,-79.3832
relay.mostro.network,40.8302,-74.1299
nostr-verified.wellorder.net,45.5201,-122.99
ynostr.yael.at,60.1699,24.9384
nostr.wild-vibes.ts.net,48.8566,2.35222
relay.mypathtofire.de,42.8864,-78.8784
relay.klabo.world,47.674,-122.122
nostr.tagomago.me,42.3601,-71.0589
relay.veganostr.com:443,60.1699,24.9384
nrs-01.darkcloudarcade.com:443,39.0997,-94.5786
relay.loveisbitcoin.com,43.6532,-79.3832
bridge.tagomago.me,42.3601,-71.0589
relay-rpi.edufeed.org:443,49.4521,11.0767
relay.kilombino.com,43.6532,-79.3832
relay.sharegap.net,43.6532,-79.3832
nostr.yutakobayashi.com,43.6532,-79.3832
relay-na1.metanomalist.com,43.6532,-79.3832
public.crostr.com:443,43.6532,-79.3832
nostrride.io,37.3986,-121.964
nostr.bgbitcoin.club:443,50.4754,12.3683
nostr.christiansass.de,50.1109,8.68213
nos.lol:443,50.4754,12.3683
relay.pyramid.li,47.4093,8.46503
relay.mappingbitcoin.com,43.6532,-79.3832
nostr-relay.zimage.com,34.282,-118.439
offchain.pub:443,39.1585,-94.5728
nostr.myshosholoza.co.za:443,52.3676,4.90414
0x-nostr-relay.fly.dev,38.7946,-106.535
relay1.privkey.io,25.7617,-80.1918
chat-relay.zap-work.com:443,43.6532,-79.3832
relay.staging.plebeian.market:443,51.5072,-0.127586
nostr.robosats.org,64.1476,-21.9392
relay.endfiat.money:443,59.3327,18.0656
nostr.agh.ccwu.cc,43.6532,-79.3832
nostr.overmind.lol:443,43.6532,-79.3832
nostr.tac.lol,47.4748,-122.273
relay.ru.ac.th,13.7607,100.627
nostr-relay.xbytez.io,50.6924,3.20113
relay.islandbitcoin.com,12.8498,77.6545
relay5.bitransfer.org,43.6532,-79.3832
prl.plus,55.7628,37.5983
chat-relay.zap-work.com,43.6532,-79.3832
relay.satlantis.io,39.0438,-77.4874
nostr.bitcoiner.social:443,40.7608,-111.891
nostr.ac,38.958,-77.3592
relay.solomonstr.com,43.6532,-79.3832
strfry.apps3.slidestr.net,40.4167,-3.70329
relay.nostr.com,50.1109,8.68213
reraw.pbla2fish.cc,43.6532,-79.3832
nostr.bond,50.1109,8.68213
relay.wavlake.com:443,41.2619,-95.8608
nos.lol,50.4754,12.3683
nostr.debate.report,50.1109,8.68213
relay-testnet.k8s.layer3.news,37.3387,-121.885
relay.lanacoin-eternity.com:443,40.8302,-74.1299
relay.wisp.talk,49.4543,11.0746
nostr.tabordalab.com,60.1699,24.9384
relay.trotters.cc,43.6532,-79.3832
nostr.4rs.nl,49.0291,8.35696
relay.hivetalk.org,40.8302,-74.1299
spamspamspamspam.rest,43.6532,-79.3832
nostr.janx.com,43.6532,-79.3832
relay.mostr.pub,43.6532,-79.3832
relay.lightning.pub:443,39.0438,-77.4874
relay.samt.st,40.8302,-74.1299
bruh.samt.st,43.6532,-79.3832
relay.orangesync.tech,40.7128,-74.006
relay-fra.zombi.cloudrodion.com,48.8566,2.35222
relay.notoshi.win,13.3396,100.93
relay.nostreon.com,60.1699,24.9384
nostr.purpura.cloud,43.6532,-79.3832
basspistol.org,49.0291,8.35696
relay.nostrfeed.com,60.1699,24.9384
nostr.dlcdevkit.com:443,40.0992,-83.1141
nostr.21crypto.ch,47.5356,8.73209
nostr.pbfs.io,50.4754,12.3683
social.amanah.eblessing.co,48.1046,11.6002
relay.cosmicbolt.net:443,37.3986,-121.964
21milionidinostr.duckdns.org,41.8967,12.4822
fanfares.nostr1.com,40.7057,-74.0136
nostr.na.social,43.6532,-79.3832
nostr.data.haus,50.4754,12.3683
strfry.shock.network:443,39.0438,-77.4874
nostr.pbfs.io:443,50.4754,12.3683
relay.shadowbip.com,50.1109,8.68213
relay.ltgnet.work:8443,51.0511,-114.075
relay02.lnfi.network,35.6764,139.65
nostr.myshosholoza.co.za,52.3676,4.90414
relay.satsmarkt.club,52.6907,4.8181
nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874
auth.nostr1.com,40.7057,-74.0136
dm-test-nostr-rs-42-disabled.samt.st,43.6532,-79.3832
bitcoinostr.duckdns.org,38.9504,-0.14007
relayrs.notoshi.win:443,43.6532,-79.3832
nostr.whitenode45.ddns.net,40.55,-74.4758
relay.mccormick.cx:443,52.3563,4.95714
nostr.2b9t.xyz:443,34.0549,-118.243
relay.internationalright-wing.org,-22.4692,-48.9875
nostr.2b9t.xyz,34.0549,-118.243
relay.mitchelltribe.com,39.0438,-77.4874
relay.nostriot.com,41.5695,-83.9786
infinity-signal-relay.digitalforlifeagency.workers.dev,43.6532,-79.3832
relay.sector01.com,41.4513,-81.7021
relay.nostx.io,43.6532,-79.3832
relay.fiatdenier.com,43.7787,-79.5393
relayone.soundhsa.com:443,39.0997,-94.5786
strfry.bonsai.com:443,39.0438,-77.4874
nostr.snowbla.de:443,50.4754,12.3683
relay.btcforplebs.com,43.6532,-79.3832
dev.relay.stream,43.6532,-79.3832
nostr-pub.wellorder.net,45.5201,-122.99
nostr.twinkle.lol,51.902,7.6657
nostr.carroarmato0.be,50.914,3.21378
bcast.girino.org,43.6532,-79.3832
treuzkas.branruz.com,48.8575,2.35138
dm-test-strfry-discovery.samt.st:443,43.6532,-79.3832
relay.endfiat.money,59.3327,18.0656
relay.jmoose.rocks,37.4419,-122.143
relay.degmods.com,50.4754,12.3683
relay.ditto.pub,43.6532,-79.3832
nostrelay.circum.space:443,52.2245,8.826
relay.pocketnostr.com,40.8054,-74.0241
nostr.wecsats.io,43.6532,-79.3832
node.kommonzenze.de,49.4521,11.0767
nostr.sathoarder.com,48.5734,7.75211
chorus.mikedilger.com:444,-36.8906,174.794
cache.trustr.ing,43.6548,-79.3885
bitcoiner.social,40.7608,-111.891
relay.tdw.gg,34.0549,-118.243
relay.edufeed.org:443,49.4521,11.0767
relay.snotr.nl:49999,51.9758,4.31389
nostr.bgbitcoin.club,50.4754,12.3683
relay.bowlafterbowl.com,32.9483,-96.7299
relay-rpi.edufeed.org,49.4521,11.0767
x.kojira.io:443,43.6532,-79.3832
nostr.self-determined.de,51.4379,14.2355
schnorr.me:443,43.6532,-79.3832
nostr.computingcache.com,45.5341,-122.956
cs-relay.nostrdev.com,50.4754,12.3683
relay2.veganostr.com,60.1699,24.9384
relay.phostrich.com,40.55,-74.4758
relay.nostrian-conquest.com,41.223,-111.974
relay.flashapp.me,43.6548,-79.3885
relay.yoinekodo.jp,43.6532,-79.3832
relay-arg.zombi.cloudrodion.com,1.35208,103.82
freelay.sovbit.host,60.1699,24.9384
nostr.n7ekb.net,47.4941,-122.294
purplerelay.com:443,43.6532,-79.3832
nostr.tbxnetworx.de,47.7228,9.96456
relay.bnos.space:443,43.6532,-79.3832
relay.satsapp.me,50.4754,12.3683
nostr-01.uid.ovh,50.9871,2.12554
tribune-panel-growing-noon.trycloudflare.com,43.6532,-79.3832
nostr.mom,50.4754,12.3683
no.str.cr,8.96171,-83.5246
nostr.spicyz.io,43.6532,-79.3832
memlay.v0l.io,53.3498,-6.26031
bucket.coracle.social,37.7775,-122.397
nostr.azzamo.net,52.2633,21.0283
relay.lanacoin-eternity.com,40.8302,-74.1299
nostr-kyomu-haskell.onrender.com,37.7775,-122.397
conduitl2.fly.dev,38.7946,-106.535
relay.wasabiwallet.io,43.6532,-79.3832
portal-relay.pareto.space,49.0291,8.35696
strfry.ymir.cloud,43.6532,-79.3832
nostr.hoppe-relay.it.com,42.8864,-78.8784
nostr.red5d.dev,43.6532,-79.3832
strfry.shock.network,39.0438,-77.4874
relay.chorus.community,48.5333,10.7
relay.mitchelltribe.com:443,39.0438,-77.4874
relay.olas.app,60.1699,24.9384
testnet-relay.samt.st:443,40.8302,-74.1299
relay.dwadziesciajeden.pl,52.2297,21.0122
relay.gulugulu.moe,43.6532,-79.3832
relay.underorion.se,50.1109,8.68213
relay.mccormick.cx,52.3563,4.95714
top.testrelay.top,43.6532,-79.3832
nostr-02.uid.ovh,50.9871,2.12554
nostr.relay.hedwig.sh,60.1699,24.9384
nostr-01.yakihonne.com,1.32123,103.695
relay.nostr.net,43.6532,-79.3832
nostr-relay.cbrx.io,43.6532,-79.3832
nrs-01.darkcloudarcade.com,39.0997,-94.5786
relay.trotters.cc:443,43.6532,-79.3832
strfry.bonsai.com,39.0438,-77.4874
relay.snort.social,53.3498,-6.26031
nostr.hekster.org,37.3986,-121.964
relay.mleku.dev,32.7767,-96.797
nostr-relay.xbytez.io:443,50.6924,3.20113
nosflare-leefcore.leefcore.workers.dev,43.6532,-79.3832
nostr.davenov.com,50.1109,8.68213
relay.chatbett.de,40.7128,-74.006
relayone.geektank.ai:443,39.0997,-94.5786
insta-relay.apps3.slidestr.net,40.4167,-3.70329
nostr.mom:443,50.4754,12.3683
nostr-relay.corb.net:443,39.6478,-104.988
relay.gulugulu.moe:443,43.6532,-79.3832
nostr.thebiglake.org,32.71,-96.6745
relay.nearhood.co.uk,51.5134,-0.0890675
relay1.nostrchat.io,60.1699,24.9384
rele.speyhard.fi,50.1109,8.68213
relay.bornheimer.app,50.1109,8.68213
nostr.fullstackcash.net,45.5201,-122.99
relay2.fiatdenier.com,50.1013,8.62643
relay.solife.me,43.6532,-79.3832
syb.lol,34.0549,-118.243
dev-relay.nostreon.com,60.1699,24.9384
relay.agorist.space:443,52.3734,4.89406
nostr.easycryptosend.it,43.6532,-79.3832
x.kojira.io,43.6532,-79.3832
adre.su,59.9311,30.3609
offchain.bostr.online,43.6532,-79.3832
relay.libernet.app,43.6532,-79.3832
nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832
nostr.rtvslawenia.com,49.4543,11.0746
relay.layer.systems,49.0291,8.35695
relay.arx-ccn.com,50.4754,12.3683
staging.yabu.me,35.6092,139.73
relay.vrtmrz.net,43.6532,-79.3832
nostr.bitcoiner.social,40.7608,-111.891
nostr.88mph.life,52.1941,-2.21905
relay.pprgb.app,52.52,13.405
relay.aarpia.com,37.3986,-121.964
nostrcity-club.fly.dev,38.7946,-106.535
buzz.ac2n-share.kozow.com,45.764,4.83566
relay.fckstate.net,59.3293,18.0686
nostr.thalheim.io:443,60.1699,24.9384
nostr.carroarmato0.be:443,50.914,3.21378
nostrcity-club.fly.dev:443,38.7946,-106.535
relay.routstr.com,59.4016,17.9455
nostr-dev.wellorder.net,45.5201,-122.99
relay.directsponsor.net,42.8864,-78.8784
relay.mwaters.net,50.9871,2.12554
relay.littlebitstudios.com,43.6532,-79.3832
relay.ohstr.com,43.6532,-79.3832
testnet-relay.samt.st,40.8302,-74.1299
relay.bullishbounty.com,43.6532,-79.3832
relay.nostrmap.net:443,60.1699,24.9384
relay.piazza.today,48.122,11.589

1 Relay URL Latitude Longitude
nostr.n7ekb.net 47.4941 -122.294
relay.kilombino.com 43.6532 -79.3832
relay02.lnfi.network 35.6764 139.65
relay.liberbitworld.org 43.6532 -79.3832
articles.layer3.news 37.3387 -121.885
relay.nostr.net 43.6532 -79.3832
nostr.easycryptosend.it 43.6532 -79.3832
strfry.bonsai.com 39.0438 -77.4874
relay.olas.app:443 60.1699 24.9384
relay.sharegap.net 43.6532 -79.3832
relay.ditto.pub 43.6532 -79.3832
nostrride.io 37.3986 -121.964
relay.favillakey.io -27.4705 153.026
nostr-relay.xbytez.io 50.6924 3.20113
relay.nostrmap.net:443 60.1699 24.9384
mostro-p2p.tech 50.1109 8.68213
relay.aidatanorge.no 43.6532 -79.3832
nostr.openhoofd.nl 51.5717 3.70417
relay.chatbett.de 40.7128 -74.006
relay.ohstr.com:443 43.6532 -79.3832
nostr.overmind.lol:443 43.6532 -79.3832
nostr.red5d.dev 43.6532 -79.3832
relay.snort.social 53.3498 -6.26031
relay.directsponsor.net 42.8864 -78.8784
relay.bullishbounty.com:443 43.6532 -79.3832
relay.scuba323.com 40.8218 -74.45
nostr.spicyz.io 43.6532 -79.3832
nostr.chaima.info 50.1109 8.68213
nostr.purpura.cloud 43.6532 -79.3832
relay.nostrops.com 32.71 -96.6745
relay.veganostr.com:443 60.1699 24.9384
relay.ohstr.com 43.6532 -79.3832
soloco.nl 43.6532 -79.3832
nostr.plantroon.com:443 50.1013 8.62643
relay.chorus.community:443 48.5333 10.7
relay.fiatdenier.com 43.7787 -79.5393
nostr.whitenode45.ddns.net 40.55 -74.4758
relay.cosmicbolt.net:443 37.3986 -121.964
relay.lanacoin-eternity.com:443 40.8302 -74.1299
relay.cosmicbolt.net 37.3986 -121.964
fanfares.nostr1.com:443 40.7057 -74.0136
nostr-kyomu-haskell.onrender.com 37.7775 -122.397
relay.keykeeper.world 40.7824 -74.0711
relay-testnet.k8s.layer3.news 37.3387 -121.885
dm-test-strfry-generic.samt.st 43.6532 -79.3832
relay.laantungir.net -19.4692 -42.5315
nostr.sathoarder.com 48.5734 7.75211
relay.mccormick.cx:443 52.3563 4.95714
relay.solife.me 43.6532 -79.3832
relay.fountain.fm:443 43.6532 -79.3832
nostrcity-club.fly.dev 38.7946 -106.535
nostr.mas-family.eu 60.3478 15.7505
no.str.cr 8.96171 -83.5246
relay.wavefunc.live 41.8781 -87.6298
relay.cyberguy.fyi 52.6907 4.8181
adre.su 59.9311 30.3609
relay.erybody.com 41.4513 -81.7021
relay.mrmave.work 43.6532 -79.3832
relay.decentralia.fr 48.122 11.589
relay.mccormick.cx 52.3563 4.95714
nostr.dlcdevkit.com:443 40.0992 -83.1141
nostr-02.yakihonne.com 1.32123 103.695
nostr.21crypto.ch 47.5356 8.73209
slick.mjex.me 39.0418 -77.4744
nostr.spicyz.io:443 43.6532 -79.3832
relay-dev.gulugulu.moe 43.6532 -79.3832
relay.nostx.io 43.6532 -79.3832
relay.wisp.talk 49.4543 11.0746
relay.plebeian.market:443 50.1109 8.68213
relay.mostr.pub:443 43.6532 -79.3832
relay.aarpia.com 37.3986 -121.964
chat-relay.zap-work.com 43.6532 -79.3832
chat-relay.zap-work.com:443 43.6532 -79.3832
relay.mitchelltribe.com:443 39.0438 -77.4874
relay.paulstephenborile.com 49.4543 11.0746
relay.trotters.cc 43.6532 -79.3832
nexus.libernet.app:443 43.6532 -79.3832
syb.lol 34.0549 -118.243
relay.cypherflow.ai 48.8575 2.35138
relay.despera.space 52.1782 20.9926
relay.loveisbitcoin.com 43.6532 -79.3832
relay.rushmi0.win 43.6532 -79.3832
relay.degmods.com 50.4754 12.3683
relay.satlantis.io 39.0438 -77.4874
relay.underorion.se 50.1109 8.68213
nostr.mom:443 50.4754 12.3683
nostr-verified.wellorder.net 45.5201 -122.99
relay.nostreon.com 60.1699 24.9384
nostr.christiansass.de 54.3213 10.1349
rilo.nostria.app:443 43.6532 -79.3832
relay.trotters.cc:443 43.6532 -79.3832
kotukonostr.onrender.com 37.7775 -122.397
relay.nmail.li 50.9871 2.12554
relay.artio.inf.unibe.ch 46.9501 7.43678
nos.lol 50.4754 12.3683
nostr.snowbla.de:443 50.4754 12.3683
relay.openresist.com 43.6532 -79.3832
ribo.us.nostria.app 43.6532 -79.3832
bitchat.nostr1.com 40.7057 -74.0136
vm-1734.lnvps.cloud 53.3498 -6.26031
nostr-relay.xbytez.io:443 50.6924 3.20113
nostr.sathoarder.com:443 48.5734 7.75211
bcast.girino.org 43.6532 -79.3832
ribo.us.nostria.app:443 43.6532 -79.3832
strfry.shock.network 39.0438 -77.4874
relay.staging.plebeian.market:443 51.5072 -0.127586
offchain.pub:443 39.1585 -94.5728
relayone.soundhsa.com 39.0997 -94.5786
relay.libernet.app 43.6532 -79.3832
relay.earthly.city 34.1749 -118.54
relayrs.notoshi.win:443 43.6532 -79.3832
nostr.vulpem.com 49.4543 11.0746
relay.getsafebox.app:443 43.6532 -79.3832
relay.paulstephenborile.com:443 49.4543 11.0746
nostr.unkn0wn.world 46.8499 9.53287
relay.binaryrobot.com 43.6532 -79.3832
relay.wellorder.net 45.5201 -122.99
relayone.geektank.ai:443 39.0997 -94.5786
relay.wasabiwallet.io 43.6532 -79.3832
top.testrelay.top 43.6532 -79.3832
nostr.2b9t.xyz 34.0549 -118.243
relay.nostr.com 50.1109 8.68213
nostr.azzamo.net:443 52.2633 21.0283
nostr.dlcdevkit.com 40.0992 -83.1141
nostr.robosats.org 64.1476 -21.9392
nostr.iskarion.ddns.net 43.3076 -2.95421
relay2.angor.io 48.1046 11.6002
relay.mmwaves.de 48.8575 2.35138
relay.agorist.space:443 52.3734 4.89406
relay.dreamith.to 43.6532 -79.3832
nostr-02.uid.ovh 50.9871 2.12554
nostr.thebiglake.org 32.71 -96.6745
nostr-relay.corb.net 39.6478 -104.988
relay.dyne.org 49.0291 8.35705
nosflare-leefcore.leefcore.workers.dev 43.6532 -79.3832
relay.angor.io 48.1046 11.6002
nostrelay.circum.space 52.2245 8.826
nostr-01.uid.ovh 50.9871 2.12554
maxq.descendant.io 43.6532 -79.3832
offchain.bostr.online 43.6532 -79.3832
relay.binaryrobot.com:443 43.6532 -79.3832
nostr.planix.org 43.6532 -79.3832
tribune-panel-growing-noon.trycloudflare.com 43.6532 -79.3832
prl.plus 55.7628 37.5983
relay.mwaters.net 50.9871 2.12554
dev.relay.stream 43.6532 -79.3832
relay.bornheimer.app 50.1109 8.68213
inbox.scuba323.com 40.8218 -74.45
relay-us.zombi.cloudrodion.com 40.7862 -74.0743
relay.nuts.cash 52.3676 4.90414
bruh.samt.st 43.6532 -79.3832
nostr.carroarmato0.be:443 50.914 3.21378
relay.plebeian.market 50.1109 8.68213
schnorr.me:443 43.6532 -79.3832
nostr.thalheim.io:443 60.1699 24.9384
nostr.88mph.life 52.1941 -2.21905
cache.trustr.ing 43.6548 -79.3885
ec2.f7z.io 60.1699 24.9384
relay.btcforplebs.com 43.6532 -79.3832
nostr.liberty.fans 36.8767 -89.5879
syb.lol:443 34.0549 -118.243
nostr.islandarea.net:443 35.4669 -97.6473
nostr.mom 50.4754 12.3683
nostr.data.haus:443 50.4754 12.3683
strfry.bonsai.com:443 39.0438 -77.4874
nostr.highway15.net 28.0445 -82.6699
relay.wavlake.com:443 41.2619 -95.8608
relay.nostriot.com:443 41.5695 -83.9786
relay.gulugulu.moe:443 43.6532 -79.3832
rele.speyhard.fi 50.1109 8.68213
nostr.hoppe-relay.it.com 42.8864 -78.8784
nostr2.girino.org:443 43.6532 -79.3832
nostr.oxtr.dev 50.4754 12.3683
nostr.bitcoiner.social:443 40.7608 -111.891
nostrrelay.taylorperron.com 45.5029 -73.5723
nostr-01.yakihonne.com:443 1.32123 103.695
nostr.tagomago.me 42.3601 -71.0589
relay.hivetalk.org 40.8302 -74.1299
relay.sigit.io:443 50.4754 12.3683
relay.openresist.com:443 43.6532 -79.3832
cs-relay.nostrdev.com:443 50.4754 12.3683
no.str.cr:443 8.96171 -83.5246
relay.nearhood.co.uk 51.5134 -0.0890675
relay.lanavault.space 60.1699 24.9384
relay.guggero.org 46.5971 9.59652
nexus.libernet.app 43.6532 -79.3832
relay.nostriot.com 41.5695 -83.9786
dm-test-strfry-discovery.samt.st:443 43.6532 -79.3832
x.kojira.io:443 43.6532 -79.3832
nostr.4rs.nl 49.0291 8.35696
relayone.geektank.ai 39.0997 -94.5786
nostr.debate.report 50.1109 8.68213
relay.gulugulu.moe 43.6532 -79.3832
public.crostr.com 43.6532 -79.3832
offchain.pub 39.1585 -94.5728
relay.tdw.lol 41.8781 -87.6298
relay5.bitransfer.org 43.6532 -79.3832
relay.manneken.brussels 49.4543 11.0746
chorus.pjv.me 45.5201 -122.99
relay.fckstate.net 59.3293 18.0686
treuzkas.branruz.com 48.8575 2.35138
nostr-relay.cbrx.io 43.6532 -79.3832
nostr-relay.nextblockvending.com 47.2343 -119.853
nostr.fullstackcash.net 45.5201 -122.99
relay.primal.net 43.6532 -79.3832
relay.mappingbitcoin.com 43.6532 -79.3832
nostr.mikoshi.de 50.1109 8.68213
nostr.stakey.net 52.3676 4.90414
nostr-rs-relay-qj1h.onrender.com 37.7775 -122.397
relay.wavlake.com 41.2619 -95.8608
relay.ordoplay.com 50.1109 8.68213
relay.klabo.world 47.674 -122.122
buzz.cashu.space 50.1109 8.68213
relay.nostrhub.fr 48.1045 11.6004
portal-relay.pareto.space 49.0291 8.35696
dev-relay.nostreon.com 60.1699 24.9384
relay.islandbitcoin.com 12.8498 77.6545
testr.nymble.world 40.8054 -74.0241
relayrs.notoshi.win 43.6532 -79.3832
nostr-relay.corb.net:443 39.6478 -104.988
cdn.satellite.earth 40.8302 -74.1299
rilo.nostria.app 43.6532 -79.3832
nostr.pbfs.io 50.4754 12.3683
nostr2.girino.org 43.6532 -79.3832
0x-nostr-relay.fly.dev 38.7946 -106.535
nostr.girino.org 43.6532 -79.3832
relay.layer.systems 49.0291 8.35695
relay.olas.app 60.1699 24.9384
ribo.eu.nostria.app 43.6532 -79.3832
buzz.ac2n-share.kozow.com 45.764 4.83566
auth.nostr1.com 40.7057 -74.0136
relay.bitmacro.cloud 43.6532 -79.3832
relay.qstr.app 50.1109 8.68213
relay.ru.ac.th 13.7607 100.627
ribo.nostria.app:443 43.6532 -79.3832
relay.44billion.net 43.6532 -79.3832
cs-relay.nostrdev.com 50.4754 12.3683
relay.nostrian-conquest.com:443 41.223 -111.974
relay.bullishbounty.com 43.6532 -79.3832
relay.edufeed.org 49.4521 11.0767
porchlight.social 43.6532 -79.3832
nostr.bitcoiner.social 40.7608 -111.891
testnet-relay.samt.st 40.8302 -74.1299
relay.vrtmrz.net 43.6532 -79.3832
nostr.wecsats.io:443 43.6532 -79.3832
relay.satmaxt.xyz:443 43.6532 -79.3832
nostr.snowbla.de 50.4754 12.3683
nostr.rtvslawenia.com:443 49.4543 11.0746
relay.flashapp.me 43.6548 -79.3885
purplerelay.com:443 43.6532 -79.3832
relay.staging.plebeian.market 51.5072 -0.127586
relay.libernet.app:443 43.6532 -79.3832
relay.0xchat.com 43.6532 -79.3832
2 myvoiceourstory.org 37.3598 -121.981
3 nrs-01.darkcloudarcade.com:443 nostr.linky.fit 39.0997 50.1109 -94.5786 8.68213
relay.vrtmrz.net:443 43.6532 -79.3832
relay.zone667.com 60.1699 24.9384
nostr.nodesmap.com 59.3327 18.0656
relay.agorist.space 52.3734 4.89406
relayone.soundhsa.com:443 39.0997 -94.5786
relay.satmaxt.xyz 43.6532 -79.3832
relay.zapanimations.com 40.8054 -74.0241
relay.sovereignresonance.org 48.8575 2.35138
nostr.relay.hedwig.sh 60.1699 24.9384
relay-dev.gulugulu.moe:443 43.6532 -79.3832
relay.arx-ccn.com 50.4754 12.3683
nip85.nosfabrica.com 39.0997 -94.5786
nrs-01.darkcloudarcade.com 39.0997 -94.5786
spamspamspamspam.rest 43.6532 -79.3832
nostr.davenov.com 50.1109 8.68213
relay.littlebitstudios.com 43.6532 -79.3832
purplerelay.com 43.6532 -79.3832
nos.lol:443 50.4754 12.3683
relay2.veganostr.com 60.1699 24.9384
relay-rpi.edufeed.org 49.4521 11.0767
relay.lacrypta.ar 43.6532 -79.3832
relay.tdw.gg 34.0549 -118.243
relay.getvia.xyz 60.1699 24.9384
relay.lanacoin-eternity.com 40.8302 -74.1299
conduitl2.fly.dev 38.7946 -106.535
nostr.rtvslawenia.com 49.4543 11.0746
relay.wavefunc.live:443 41.8781 -87.6298
nostr.carroarmato0.be 50.914 3.21378
nostr.pbfs.io:443 50.4754 12.3683
ribo.nostria.app 43.6532 -79.3832
nostr.sovereignservices.xyz 43.6532 -79.3832
relay.mostr.pub 43.6532 -79.3832
schnorr.me 43.6532 -79.3832
x.kojira.io 43.6532 -79.3832
4 public.obelisk.ar 43.6532 -79.3832
5 relay.angor.io:443 relay.i9.eti.br 48.1046 43.6532 11.6002 -79.3832
nostr.oxtr.dev:443 50.4754 12.3683
nostr.ltd 19.076 72.8777
nostr.tac.lol:443 47.4748 -122.273
nostr.twinkle.lol 51.902 7.6657
relay.s-w.art 43.6532 -79.3832
relay.openfarmtools.org 60.1699 24.9384
nostr.hekster.org 37.3986 -121.964
strfry.apps3.slidestr.net 40.4167 -3.70329
relay.mypathtofire.de 42.8864 -78.8784
nostr.wecsats.io 43.6532 -79.3832
nostr.computingcache.com 34.0356 -118.442
nostr.janx.com 43.6532 -79.3832
bread.nostrsms.com 40.8218 -74.45
relay.chorus.community 48.5333 10.7
testnet-relay.samt.st:443 40.8302 -74.1299
relay.kaleidoswap.com 50.8476 4.35717
6 yabu.me 35.6092 139.73
7 nostr-dev.wellorder.net relay-us.zombi.cloudrodion.com 45.5201 40.7862 -122.99 -74.0743
relay.layer.systems:443 49.0291 8.35695
relay-can.zombi.cloudrodion.com 43.6532 -79.3832
nostr-relay.zimage.com 34.282 -118.439
relay.fundstr.me 42.3601 -71.0589
nostr.stakey.net:443 52.3676 4.90414
relay.satsmarkt.club 52.6907 4.8181
relay.satsapp.me 50.4754 12.3683
bendernostur.duckdns.org:8443 50.1109 8.68213
relay.illuminodes.com 43.6532 -79.3832
nostr.chaima.info:443 50.1109 8.68213
nostr.hekster.org:443 37.3986 -121.964
nostr.yutakobayashi.com 43.6532 -79.3832
freelay.sovbit.host 60.1699 24.9384
insta-relay.apps3.slidestr.net 40.4167 -3.70329
infinity-signal-relay.digitalforlifeagency.workers.dev 43.6532 -79.3832
relay.internationalright-wing.org -22.4692 -48.9875
relay.mmwaves.de:443 48.8575 2.35138
public.crostr.com:443 43.6532 -79.3832
relay.nostrian-conquest.com 41.223 -111.974
relay-rpi.edufeed.org:443 49.4521 11.0767
relay2.angor.io:443 48.1046 11.6002
relay-fra.zombi.cloudrodion.com 48.8566 2.35222
bunk-test.feeds.relay.tools 38.6327 -90.1961
nostr-pr02.redscrypt.org 52.3676 4.90414
nostr.tac.lol 47.4748 -122.273
nostr.overpay.com 29.7449 -95.5343
relay.novospes.com 43.6532 -79.3832
nostrcity-club.fly.dev:443 38.7946 -106.535
relay01.lnfi.network 35.6764 139.65
relay.lightning.pub:443 39.0438 -77.4874
relay.pyramid.li 47.4093 8.46503
8 relay.nostu.be 40.4167 -3.70329
9 nostr-rs-relay.dev.fedibtc.com:443 relay.favillakey.io 39.0438 -27.4705 -77.4874 153.026
10 relay.endfiat.money:443 nostr.mikoshi.de 59.3327 50.1109 18.0656 8.68213
11 nostr.spaceshell.xyz relay.agilesolutionlabs.com 43.6532 -79.3832
12 21milionidinostr.duckdns.org nostr.sovereignservices.xyz 45.5192 43.6532 9.08625 -79.3832
13 relay.qstr.app 50.1109 8.68213
14 articles.layer3.news:443 37.3387 -121.885
15 wheat.happytavern.co relay.nostrops.com 43.6532 32.71 -79.3832 -96.6745
16 nostr-rs-relay.dev.fedibtc.com relay.mrmave.work 39.0438 43.6532 -77.4874 -79.3832
17 nostr.na.social nostr.islandarea.net:443 43.6532 35.4669 -79.3832 -97.6473
18 social.amanah.eblessing.co nostr.azzamo.net:443 48.1046 52.2633 11.6002 21.0283
19 reraw.pbla2fish.cc ec2.f7z.io 43.6532 60.1699 -79.3832 24.9384
nostr.girino.org:443 43.6532 -79.3832
staging.yabu.me 35.6092 139.73
nostr.islandarea.net 35.4669 -97.6473
relay.endfiat.money 59.3327 18.0656
relay.mitchelltribe.com 39.0438 -77.4874
nostr.myshosholoza.co.za 52.3676 4.90414
relay.staging.commonshub.brussels 49.4543 11.0746
bridge.tagomago.me 42.3601 -71.0589
relay.agentry.com 42.8864 -78.8784
testnet.samt.st 43.6532 -79.3832
nostr.wild-vibes.ts.net 48.8566 2.35222
relay.getsafebox.app 43.6532 -79.3832
relay.notoshi.win 13.3396 100.93
bitcoinostr.duckdns.org 41.1976 1.11167
relay.sigit.io 50.4754 12.3683
bucket.coracle.social 37.7775 -122.397
dm-test-strfry-discovery.samt.st 43.6532 -79.3832
vault.iris.to:443 43.6532 -79.3832
relay.wisp.talk:443 49.4543 11.0746
relay.mostro.network 40.8302 -74.1299
nostr.l484.com 30.2672 -97.7431
relay.froth.zone 60.1699 24.9384
relay.beamhop.com 43.6532 -79.3832
relay.sincensura.org 43.6532 -79.3832
relay.ditto.pub:443 43.6532 -79.3832
nostr.azzamo.net 52.2633 21.0283
relay.edufeed.org:443 49.4521 11.0767
ribo.eu.nostria.app:443 43.6532 -79.3832
vault.iris.to 43.6532 -79.3832
nostr.plantroon.com 50.1013 8.62643
fanfares.nostr1.com 40.7057 -74.0136
budabit.nostr1.com 40.7057 -74.0136
relay.damus.io 43.6532 -79.3832
strfry.shock.network:443 39.0438 -77.4874
bitcoiner.social 40.7608 -111.891
basspistol.org 49.0291 8.35696
nostr.thalheim.io 60.1699 24.9384
relay.conduit.market 38.7946 -106.535
relay.yoinekodo.jp 43.6532 -79.3832
nostrbtc.com 43.6532 -79.3832
relay.lightning.pub 39.0438 -77.4874
nostr-01.yakihonne.com 1.32123 103.695
nostr-pub.wellorder.net 45.5201 -122.99
strfry.ymir.cloud 43.6532 -79.3832
relay.minibolt.info 43.6532 -79.3832
nostr.data.haus 50.4754 12.3683
relay.otrta.me 50.1109 8.68213
relay.samt.st 40.8302 -74.1299
relay.nostr.blockhenge.com 39.0438 -77.4874
relay.minibolt.info:443 43.6532 -79.3832
nostr.2b9t.xyz:443 34.0549 -118.243
dm-test-nostr-rs-42-disabled.samt.st 43.6532 -79.3832
20 relay.veganostr.com 60.1699 24.9384
21 relay.fountain.fm nostr.dlcdevkit.com 43.6532 40.0992 -79.3832 -83.1141
22 node.kommonzenze.de relay.44billion.net 49.4521 43.6532 11.0767 -79.3832
23 relay-arg.zombi.cloudrodion.com bendernostur.duckdns.org:8443 1.35208 50.1109 103.82 8.68213
24 nostr.myshosholoza.co.za:443 nostras.app 52.3676 38.7946 4.90414 -106.535
25 temp.iris.to cs-relay.nostrdev.com:443 43.6532 50.4754 -79.3832 12.3683
26 nostr.stakey.net 52.3676 4.90414
27 support.flotilla.social 32.9483 -96.7299
28 relay.ordoplay.com 50.1109 8.68213
29 nostr.stakey.net:443 52.3676 4.90414
30 relay.nostrmap.net 60.1699 24.9384
31 relayone.geektank.ai 39.0997 -94.5786
32 relay.novospes.com 43.6532 -79.3832
33 relay.dyne.org 49.0291 8.35705
34 nostrrelay.taylorperron.com 45.5029 -73.5723
35 nostrbtc.com 43.6532 -79.3832
36 relay.lanavault.space 60.1699 24.9384
37 relay.bullishbounty.com:443 43.6532 -79.3832
38 relay.layer.systems:443 49.0291 8.35695
39 vault.iris.to:443 43.6532 -79.3832
40 relay.getsafebox.app:443 43.6532 -79.3832
41 relay.wellorder.net 45.5201 -122.99
42 relay.scuba323.com 40.8218 -74.45
43 dm-test-strfry-discovery.samt.st 43.6532 -79.3832
44 temp.iris.to 43.6532 -79.3832
45 relay.paulstephenborile.com 49.4543 11.0746
46 relay.cyberguy.fyi 52.6907 4.8181
47 nostr.wecsats.io:443 43.6532 -79.3832
48 bread.nostrsms.com 40.8218 -74.45
49 relay.plebeian.market 50.1109 8.68213
50 relay.laantungir.net -19.4692 -42.5315
51 nostr.overmind.lol 43.6532 -79.3832
52 nip85.nosfabrica.com 39.0997 -94.5786
53 relay.conduit.market 38.7946 -106.535
54 relay.sigit.io 50.4754 12.3683
55 relay.dreamith.to 43.6532 -79.3832
56 relay.decentralia.fr 48.122 11.589
57 nexus.libernet.app 43.6532 -79.3832
58 relay.nmail.li 50.9871 2.12554
59 nostr-rs-relay.dev.fedibtc.com 39.0438 -77.4874
60 nostr-relay.nextblockvending.com 47.2343 -119.853
61 nostr-relay.corb.net 39.6478 -104.988
62 relay.agentry.com 42.8864 -78.8784
63 budabit.nostr1.com 40.7057 -74.0136
64 relay.cypherflow.ai 48.8575 2.35138
65 purplerelay.com 43.6532 -79.3832
66 nostr.islandarea.net 35.4669 -97.6473
67 relay.edufeed.org 49.4521 11.0767
68 relay.bnos.space 43.6532 -79.3832
69 relay.kizuna-miki.com 35.694 139.754
70 nostr.snowbla.de 50.4754 12.3683
71 bitchat.nostr1.com 40.7057 -74.0136
72 relay.beamhop.com 43.6532 -79.3832
73 relay.wavefunc.live 41.8781 -87.6298
74 relayrs.notoshi.win 43.6532 -79.3832
75 relay.nostrhub.fr 48.1045 11.6004
76 relay01.lnfi.network 35.6764 139.65
77 relay.bitos.space 43.6532 -79.3832
78 relay.noeudlibre.fr 50.6924 3.20113
79 relay.orly.dev 32.7767 -96.797
80 nostrelay.circum.space:443 relay.paulstephenborile.com:443 52.2245 49.4543 8.826 11.0746
81 relay.atsocy.com 43.6532 -79.3832
82 relay.keykeeper.world 40.7824 -74.0711
83 nostrmxn.lulus.com.mx 59.4016 17.9455
84 maxq.descendant.io 43.6532 -79.3832
85 nostr.spicyz.io:443 43.6532 -79.3832
86 nostr.chaima.info:443 50.1109 8.68213
87 relay.opmaat.org 60.1699 24.9384
88 nostr.notribe.net 40.8302 -74.1299
89 nostr-01.yakihonne.com:443 1.32123 103.695
90 offchain.pub 39.1585 -94.5728
91 relay.agora.social 50.7383 15.0648
92 relay.tdw.lol 41.8781 -87.6298
93 nostr.nodesmap.com 59.3327 18.0656
94 relay.getvia.xyz 60.1699 24.9384
95 public.crostr.com 43.6532 -79.3832
96 relay.agorist.space 52.3734 4.89406
97 relay.lacrypta.ar 43.6532 -79.3832
98 nostr.spaceshell.xyz 43.6532 -79.3832
99 relay.nuts.cash 52.3676 4.90414
100 relay.otrta.me 50.1109 8.68213
101 relay.satmaxt.xyz 43.6532 -79.3832
102 relay.earthly.city 34.1749 -118.54
103 relay.wisp.talk:443 49.4543 11.0746
104 mostro-p2p.tech 50.1109 8.68213
105 nostr.thalheim.io 60.1699 24.9384
106 relay.s-w.art 43.6532 -79.3832
107 nostr.chaima.info 50.1109 8.68213
108 articles.layer3.news 37.3387 -121.885
109 nostr2.girino.org:443 43.6532 -79.3832
110 relay.mostr.pub:443 43.6532 -79.3832
111 relay.ditto.pub:443 43.6532 -79.3832
112 relay.manneken.brussels 49.4543 11.0746
113 relay.kaleidoswap.com 50.8476 4.35717
114 nostrelay.circum.space 52.2245 8.826
115 relay.beginningend.com 35.2227 -97.4786
116 relay-dev.gulugulu.moe 43.6532 -79.3832
117 relay.fundstr.me 42.3601 -71.0589
118 relay.jmoose.rocks:443 37.4419 -122.143
119 relay.nostr.dev.br 48.1046 11.6002
120 relay.minibolt.info 43.6532 -79.3832
121 nostr.unkn0wn.world 46.8499 9.53287
122 vault.iris.to 43.6532 -79.3832
123 relay.guggero.org 46.5971 9.59652
124 relay.getsafebox.app 43.6532 -79.3832
125 relay.bitmacro.cloud 43.6532 -79.3832
126 relay.wavlake.com 41.2619 -95.8608
127 nostr.hekster.org:443 37.3986 -121.964
128 relay.lightning.pub 39.0438 -77.4874
129 relay-dev.gulugulu.moe:443 43.6532 -79.3832
130 soloco.nl 43.6532 -79.3832
131 nostr.hifish.org 47.4009 8.57814
132 relay-can.zombi.cloudrodion.com 43.6532 -79.3832
133 dm-test-strfry-generic.samt.st 43.6532 -79.3832
134 relay.wavefunc.live:443 41.8781 -87.6298
135 porchlight.social 43.6532 -79.3832
136 relay.zone667.com 60.1699 24.9384
137 nostr.data.haus:443 50.4754 12.3683
138 relay.nexterz.com 43.6532 -79.3832
139 relay.sigit.io:443 50.4754 12.3683
140 nostr.girino.org 43.6532 -79.3832
141 nostr.liberty.fans 36.8767 -89.5879
142 wheat.happytavern.co 43.6532 -79.3832
143 relay.0xchat.com 43.6532 -79.3832
144 inbox.scuba323.com 40.8218 -74.45
145 nostr.vulpem.com 49.4543 11.0746
146 relay.damustr.com 43.6532 -79.3832
147 no.str.cr:443 8.96171 -83.5246
148 relay.vrtmrz.net:443 43.6532 -79.3832
149 vm-1734.lnvps.cloud 53.3498 -6.26031
150 testnet.samt.st 43.6532 -79.3832
151 nostr.rtvslawenia.com:443 49.4543 11.0746
152 nostr.sathoarder.com:443 48.5734 7.75211
153 relay.cosmicbolt.net 37.3986 -121.964
154 relay.staging.plebeian.market 51.5072 -0.127586
155 relayone.soundhsa.com 39.0997 -94.5786
156 relay.olas.app:443 60.1699 24.9384
157 relay.illuminodes.com 43.6532 -79.3832
158 relay.chorus.community:443 48.5333 10.7
159 relay.ohstr.com:443 43.6532 -79.3832
160 fanfares.nostr1.com:443 40.7057 -74.0136
161 nostr.planix.org 43.6532 -79.3832
162 schnorr.me 43.6532 -79.3832
163 relay.mostro.network 40.8302 -74.1299
164 nostr-verified.wellorder.net 45.5201 -122.99
165 ynostr.yael.at 60.1699 24.9384
166 nostr.wild-vibes.ts.net 48.8566 2.35222
167 relay.mypathtofire.de 42.8864 -78.8784
168 relay.klabo.world 47.674 -122.122
169 nostr.tagomago.me 42.3601 -71.0589
170 relay.veganostr.com:443 60.1699 24.9384
171 nrs-01.darkcloudarcade.com:443 39.0997 -94.5786
172 relay.loveisbitcoin.com 43.6532 -79.3832
173 bridge.tagomago.me 42.3601 -71.0589
174 relay-rpi.edufeed.org:443 49.4521 11.0767
175 relay.kilombino.com 43.6532 -79.3832
176 relay.sharegap.net 43.6532 -79.3832
177 nostr.yutakobayashi.com 43.6532 -79.3832
178 relay-na1.metanomalist.com 43.6532 -79.3832
179 public.crostr.com:443 43.6532 -79.3832
180 nostrride.io 37.3986 -121.964
181 nostr.bgbitcoin.club:443 50.4754 12.3683
182 nostr.christiansass.de 50.1109 8.68213
183 nos.lol:443 50.4754 12.3683
184 relay.pyramid.li 47.4093 8.46503
185 relay.mappingbitcoin.com 43.6532 -79.3832
186 nostr-relay.zimage.com 34.282 -118.439
187 offchain.pub:443 39.1585 -94.5728
188 nostr.myshosholoza.co.za:443 52.3676 4.90414
189 0x-nostr-relay.fly.dev 38.7946 -106.535
190 relay1.privkey.io 25.7617 -80.1918
191 chat-relay.zap-work.com:443 43.6532 -79.3832
192 relay.staging.plebeian.market:443 51.5072 -0.127586
193 nostr.robosats.org 64.1476 -21.9392
194 relay.endfiat.money:443 59.3327 18.0656
195 nostr.agh.ccwu.cc 43.6532 -79.3832
196 nostr.overmind.lol:443 43.6532 -79.3832
197 nostr.tac.lol 47.4748 -122.273
198 relay.ru.ac.th 13.7607 100.627
199 nostr-relay.xbytez.io 50.6924 3.20113
200 relay.islandbitcoin.com 12.8498 77.6545
201 relay5.bitransfer.org 43.6532 -79.3832
202 prl.plus 55.7628 37.5983
203 chat-relay.zap-work.com 43.6532 -79.3832
204 relay.satlantis.io 39.0438 -77.4874
205 nostr.bitcoiner.social:443 40.7608 -111.891
206 nostr.ac 38.958 -77.3592
207 relay.solomonstr.com 43.6532 -79.3832
208 strfry.apps3.slidestr.net 40.4167 -3.70329
209 relay.nostr.com 50.1109 8.68213
210 reraw.pbla2fish.cc 43.6532 -79.3832
211 nostr.bond 50.1109 8.68213
212 relay.wavlake.com:443 41.2619 -95.8608
213 nos.lol 50.4754 12.3683
214 nostr.debate.report 50.1109 8.68213
215 relay-testnet.k8s.layer3.news 37.3387 -121.885
216 relay.lanacoin-eternity.com:443 40.8302 -74.1299
217 relay.wisp.talk 49.4543 11.0746
218 nostr.tabordalab.com 60.1699 24.9384
219 relay.trotters.cc 43.6532 -79.3832
220 nostr.4rs.nl 49.0291 8.35696
221 relay.hivetalk.org 40.8302 -74.1299
222 spamspamspamspam.rest 43.6532 -79.3832
223 nostr.janx.com 43.6532 -79.3832
224 relay.mostr.pub 43.6532 -79.3832
225 relay.lightning.pub:443 39.0438 -77.4874
226 relay.samt.st 40.8302 -74.1299
227 bruh.samt.st 43.6532 -79.3832
228 relay.orangesync.tech 40.7128 -74.006
229 relay-fra.zombi.cloudrodion.com 48.8566 2.35222
230 relay.notoshi.win 13.3396 100.93
231 relay.nostreon.com 60.1699 24.9384
232 nostr.purpura.cloud 43.6532 -79.3832
233 basspistol.org 49.0291 8.35696
234 relay.nostrfeed.com 60.1699 24.9384
235 nostr.dlcdevkit.com:443 40.0992 -83.1141
236 nostr.21crypto.ch 47.5356 8.73209
237 nostr.pbfs.io 50.4754 12.3683
238 social.amanah.eblessing.co 48.1046 11.6002
239 relay.cosmicbolt.net:443 37.3986 -121.964
240 21milionidinostr.duckdns.org 41.8967 12.4822
241 fanfares.nostr1.com 40.7057 -74.0136
242 nostr.na.social 43.6532 -79.3832
243 nostr.data.haus 50.4754 12.3683
244 strfry.shock.network:443 39.0438 -77.4874
245 nostr.pbfs.io:443 50.4754 12.3683
246 relay.shadowbip.com 50.1109 8.68213
247 relay.ltgnet.work:8443 51.0511 -114.075
248 relay02.lnfi.network 35.6764 139.65
249 nostr.myshosholoza.co.za 52.3676 4.90414
250 relay.satsmarkt.club 52.6907 4.8181
251 nostr-rs-relay.dev.fedibtc.com:443 39.0438 -77.4874
252 auth.nostr1.com 40.7057 -74.0136
253 dm-test-nostr-rs-42-disabled.samt.st 43.6532 -79.3832
254 bitcoinostr.duckdns.org 38.9504 -0.14007
255 relayrs.notoshi.win:443 43.6532 -79.3832
256 nostr.whitenode45.ddns.net 40.55 -74.4758
257 relay.mccormick.cx:443 52.3563 4.95714
258 nostr.2b9t.xyz:443 34.0549 -118.243
259 relay.internationalright-wing.org -22.4692 -48.9875
260 nostr.2b9t.xyz 34.0549 -118.243
261 relay.mitchelltribe.com 39.0438 -77.4874
262 relay.nostriot.com 41.5695 -83.9786
263 infinity-signal-relay.digitalforlifeagency.workers.dev 43.6532 -79.3832
264 relay.sector01.com 41.4513 -81.7021
265 relay.nostx.io 43.6532 -79.3832
266 relay.fiatdenier.com 43.7787 -79.5393
267 relayone.soundhsa.com:443 39.0997 -94.5786
268 strfry.bonsai.com:443 39.0438 -77.4874
269 nostr.snowbla.de:443 50.4754 12.3683
270 relay.btcforplebs.com 43.6532 -79.3832
271 dev.relay.stream 43.6532 -79.3832
272 nostr-pub.wellorder.net 45.5201 -122.99
273 nostr.twinkle.lol 51.902 7.6657
274 nostr.carroarmato0.be 50.914 3.21378
275 bcast.girino.org 43.6532 -79.3832
276 treuzkas.branruz.com 48.8575 2.35138
277 dm-test-strfry-discovery.samt.st:443 43.6532 -79.3832
278 relay.endfiat.money 59.3327 18.0656
279 relay.jmoose.rocks 37.4419 -122.143
280 relay.degmods.com 50.4754 12.3683
281 relay.ditto.pub 43.6532 -79.3832
282 nostrelay.circum.space:443 52.2245 8.826
283 relay.pocketnostr.com 40.8054 -74.0241
284 nostr.wecsats.io 43.6532 -79.3832
285 node.kommonzenze.de 49.4521 11.0767
286 nostr.sathoarder.com 48.5734 7.75211
287 chorus.mikedilger.com:444 -36.8906 174.794
288 cache.trustr.ing 43.6548 -79.3885
289 bitcoiner.social 40.7608 -111.891
290 relay.tdw.gg 34.0549 -118.243
291 relay.edufeed.org:443 49.4521 11.0767
292 relay.snotr.nl:49999 51.9758 4.31389
293 nostr.bgbitcoin.club 50.4754 12.3683
294 relay.bowlafterbowl.com 32.9483 -96.7299
295 relay-rpi.edufeed.org 49.4521 11.0767
296 x.kojira.io:443 43.6532 -79.3832
297 nostr.self-determined.de 51.4379 14.2355
298 schnorr.me:443 43.6532 -79.3832
299 nostr.computingcache.com 45.5341 -122.956
300 cs-relay.nostrdev.com 50.4754 12.3683
301 relay2.veganostr.com 60.1699 24.9384
302 relay.phostrich.com 40.55 -74.4758
303 relay.nostrian-conquest.com 41.223 -111.974
304 relay.flashapp.me 43.6548 -79.3885
305 relay.yoinekodo.jp 43.6532 -79.3832
306 relay-arg.zombi.cloudrodion.com 1.35208 103.82
307 freelay.sovbit.host 60.1699 24.9384
308 nostr.n7ekb.net 47.4941 -122.294
309 purplerelay.com:443 43.6532 -79.3832
310 nostr.tbxnetworx.de 47.7228 9.96456
311 relay.bnos.space:443 43.6532 -79.3832
312 relay.satsapp.me 50.4754 12.3683
313 nostr-01.uid.ovh 50.9871 2.12554
314 tribune-panel-growing-noon.trycloudflare.com 43.6532 -79.3832
315 nostr.mom 50.4754 12.3683
316 no.str.cr 8.96171 -83.5246
317 nostr.spicyz.io 43.6532 -79.3832
318 memlay.v0l.io 53.3498 -6.26031
319 bucket.coracle.social 37.7775 -122.397
320 nostr.azzamo.net 52.2633 21.0283
321 relay.lanacoin-eternity.com 40.8302 -74.1299
322 nostr-kyomu-haskell.onrender.com 37.7775 -122.397
323 conduitl2.fly.dev 38.7946 -106.535
324 relay.wasabiwallet.io 43.6532 -79.3832
325 portal-relay.pareto.space 49.0291 8.35696
326 strfry.ymir.cloud 43.6532 -79.3832
327 nostr.hoppe-relay.it.com 42.8864 -78.8784
328 nostr.red5d.dev 43.6532 -79.3832
329 strfry.shock.network 39.0438 -77.4874
330 relay.chorus.community 48.5333 10.7
331 relay.mitchelltribe.com:443 39.0438 -77.4874
332 relay.olas.app 60.1699 24.9384
333 testnet-relay.samt.st:443 40.8302 -74.1299
334 relay.dwadziesciajeden.pl 52.2297 21.0122
335 relay.gulugulu.moe 43.6532 -79.3832
336 relay.underorion.se 50.1109 8.68213
337 relay.mccormick.cx 52.3563 4.95714
338 top.testrelay.top 43.6532 -79.3832
339 nostr-02.uid.ovh 50.9871 2.12554
340 nostr.relay.hedwig.sh 60.1699 24.9384
341 nostr-01.yakihonne.com 1.32123 103.695
342 relay.nostr.net 43.6532 -79.3832
343 nostr-relay.cbrx.io 43.6532 -79.3832
344 nrs-01.darkcloudarcade.com 39.0997 -94.5786
345 relay.trotters.cc:443 43.6532 -79.3832
346 strfry.bonsai.com 39.0438 -77.4874
347 relay.snort.social 53.3498 -6.26031
348 nostr.hekster.org 37.3986 -121.964
349 relay.mleku.dev 32.7767 -96.797
350 nostr-relay.xbytez.io:443 50.6924 3.20113
351 nosflare-leefcore.leefcore.workers.dev 43.6532 -79.3832
352 nostr.davenov.com 50.1109 8.68213
353 relay.chatbett.de 40.7128 -74.006
354 relayone.geektank.ai:443 39.0997 -94.5786
355 insta-relay.apps3.slidestr.net 40.4167 -3.70329
356 nostr.mom:443 50.4754 12.3683
357 nostr-relay.corb.net:443 39.6478 -104.988
358 relay.gulugulu.moe:443 43.6532 -79.3832
359 nostr.thebiglake.org 32.71 -96.6745
360 relay.nearhood.co.uk 51.5134 -0.0890675
361 relay1.nostrchat.io 60.1699 24.9384
362 rele.speyhard.fi 50.1109 8.68213
363 relay.bornheimer.app 50.1109 8.68213
364 nostr.fullstackcash.net 45.5201 -122.99
365 relay2.fiatdenier.com 50.1013 8.62643
366 relay.solife.me 43.6532 -79.3832
367 syb.lol 34.0549 -118.243
368 dev-relay.nostreon.com 60.1699 24.9384
369 relay.agorist.space:443 52.3734 4.89406
370 nostr.easycryptosend.it 43.6532 -79.3832
371 x.kojira.io 43.6532 -79.3832
372 adre.su 59.9311 30.3609
373 offchain.bostr.online 43.6532 -79.3832
374 relay.libernet.app 43.6532 -79.3832
375 nostr-rs-relay-ishosta.phamthanh.me 43.6532 -79.3832
376 nostr.rtvslawenia.com 49.4543 11.0746
377 relay.layer.systems 49.0291 8.35695
378 relay.arx-ccn.com 50.4754 12.3683
379 staging.yabu.me 35.6092 139.73
380 relay.vrtmrz.net 43.6532 -79.3832
381 nostr.bitcoiner.social 40.7608 -111.891
382 nostr.88mph.life 52.1941 -2.21905
383 relay.pprgb.app 52.52 13.405
384 relay.aarpia.com 37.3986 -121.964
385 nostrcity-club.fly.dev 38.7946 -106.535
386 buzz.ac2n-share.kozow.com 45.764 4.83566
387 relay.fckstate.net 59.3293 18.0686
388 nostr.thalheim.io:443 60.1699 24.9384
389 nostr.carroarmato0.be:443 50.914 3.21378
390 nostrcity-club.fly.dev:443 38.7946 -106.535
391 relay.routstr.com 59.4016 17.9455
392 nostr-dev.wellorder.net 45.5201 -122.99
393 relay.directsponsor.net 42.8864 -78.8784
394 relay.mwaters.net 50.9871 2.12554
395 relay.littlebitstudios.com 43.6532 -79.3832
396 relay.ohstr.com 43.6532 -79.3832
397 testnet-relay.samt.st 40.8302 -74.1299
398 relay.bullishbounty.com 43.6532 -79.3832
399 relay.nostrmap.net:443 60.1699 24.9384
400 relay.piazza.today 48.122 11.589

View File

@ -29,9 +29,23 @@ data class PeerCapabilities(val rawValue: Long) : Parcelable {
companion object { companion object {
val NONE = PeerCapabilities(0) val NONE = PeerCapabilities(0)
val PREKEYS = PeerCapabilities(1L shl 0)
val WIFI_BULK = PeerCapabilities(1L shl 1)
val GATEWAY = PeerCapabilities(1L shl 2)
val GROUPS = PeerCapabilities(1L shl 3)
val BOARD = PeerCapabilities(1L shl 4)
val VOUCH = PeerCapabilities(1L shl 5)
val MESH_DIAGNOSTICS = PeerCapabilities(1L shl 6)
val BRIDGE = PeerCapabilities(1L shl 7)
/** Noise-encrypted private BitchatFilePacket using payload type 0x20. */ /** Noise-encrypted private BitchatFilePacket using payload type 0x20. */
val PRIVATE_MEDIA = PeerCapabilities(1L shl 8) val PRIVATE_MEDIA = PeerCapabilities(1L shl 8)
val PRIVATE_MEDIA_RECEIPTS = PeerCapabilities(1L shl 9)
/** Reserved by iOS; decode it but do not advertise or act on it. */
val NON_DESTRUCTIVE_NOISE_REPLACEMENT = PeerCapabilities(1L shl 10)
/** Capabilities implemented by this Android build. */ /** Capabilities implemented by this Android build. */
val LOCAL_SUPPORTED = PRIVATE_MEDIA val LOCAL_SUPPORTED = PRIVATE_MEDIA

View File

@ -3,8 +3,11 @@ package com.bitchat.android.nostr
import android.app.Application import android.app.Application
import android.util.Log import android.util.Log
import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.BitchatMessage
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Date import java.util.Date
/** /**
@ -18,7 +21,8 @@ class GeohashMessageHandler(
private val repo: GeohashRepository, private val repo: GeohashRepository,
private val scope: CoroutineScope, private val scope: CoroutineScope,
private val dataManager: com.bitchat.android.ui.DataManager, private val dataManager: com.bitchat.android.ui.DataManager,
private val addChannelMessage: (String, BitchatMessage) -> Unit private val addChannelMessage: (String, BitchatMessage) -> Unit,
private val signatureVerificationDispatcher: CoroutineDispatcher = Dispatchers.Default
) { ) {
companion object { private const val TAG = "GeohashMessageHandler" } companion object { private const val TAG = "GeohashMessageHandler" }
@ -44,6 +48,13 @@ class GeohashMessageHandler(
if (event.kind != NostrKind.EPHEMERAL_EVENT && event.kind != NostrKind.GEOHASH_PRESENCE) return@launch if (event.kind != NostrKind.EPHEMERAL_EVENT && event.kind != NostrKind.GEOHASH_PRESENCE) return@launch
val tagGeo = event.tags.firstOrNull { it.size >= 2 && it[0] == "g" }?.getOrNull(1) val tagGeo = event.tags.firstOrNull { it.size >= 2 && it[0] == "g" }?.getOrNull(1)
if (tagGeo == null || !tagGeo.equals(subscribedGeohash, true)) return@launch if (tagGeo == null || !tagGeo.equals(subscribedGeohash, true)) return@launch
val hasValidSignature = withContext(signatureVerificationDispatcher) {
event.isValidSignature()
}
if (!hasValidSignature) {
Log.w(TAG, "Rejecting geohash event ${event.id.take(8)}... with invalid signature")
return@launch
}
if (dedupe(event.id)) return@launch if (dedupe(event.id)) return@launch
// PoW validation (if enabled) - apply to chat messages primarily // PoW validation (if enabled) - apply to chat messages primarily

View File

@ -416,6 +416,11 @@ class LocationNotesManager private constructor() {
Log.v(TAG, "Ignoring non-text-note event: kind=${event.kind}") Log.v(TAG, "Ignoring non-text-note event: kind=${event.kind}")
return return
} }
if (!event.isValidSignature()) {
Log.w(TAG, "Rejecting note ${event.id.take(8)}... with invalid signature")
return
}
// Check for geohash tag // Check for geohash tag
val geohashTag = event.tags.firstOrNull { it.size >= 2 && it[0] == "g" } val geohashTag = event.tags.firstOrNull { it.size >= 2 && it[0] == "g" }

View File

@ -282,6 +282,11 @@ class NostrClient private constructor(private val context: Context) {
handler: (content: String, senderPubkey: String, nickname: String?, timestamp: Int) -> Unit handler: (content: String, senderPubkey: String, nickname: String?, timestamp: Int) -> Unit
) { ) {
try { try {
if (!event.isValidSignature()) {
Log.w(TAG, "🚫 Rejecting geohash event ${event.id.take(8)}... with invalid signature")
return
}
// Check Proof of Work validation for incoming geohash events // Check Proof of Work validation for incoming geohash events
val powSettings = PoWPreferenceManager.getCurrentSettings() val powSettings = PoWPreferenceManager.getCurrentSettings()
if (powSettings.enabled && powSettings.difficulty > 0) { if (powSettings.enabled && powSettings.difficulty > 0) {

View File

@ -1,6 +1,7 @@
package com.bitchat.android.protocol package com.bitchat.android.protocol
import android.os.Parcelable import android.os.Parcelable
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
@ -34,6 +35,27 @@ object SpecialRecipients {
val BROADCAST = ByteArray(8) { 0xFF.toByte() } // All 0xFF = broadcast val BROADCAST = ByteArray(8) { 0xFF.toByte() } // All 0xFF = broadcast
} }
/**
* Payload as it arrived on the wire, set by [BinaryProtocol.decode].
*
* Signatures cover a re-encoding of the packet, and verification re-encodes too.
* DEFLATE output is not canonical and clients use different encoders
* (java.util.zip.Deflater here, Apple's compression_encode_buffer on iOS), so
* re-compressing can change the preimage and reject a valid packet. Reusing these
* bytes also stops a relay, which re-encodes on TTL decrement, from substituting
* its own encoding.
*
* [forPayload] ties the bytes to the payload they decode to: replace the payload
* and the encoder compresses instead.
*
* Not a data class: generated equals/hashCode over ByteArray compares references.
*/
class WirePayload(
val bytes: ByteArray,
val compressed: Boolean,
val forPayload: ByteArray
)
/** /**
* Binary packet format - 100% backward compatible with iOS version * Binary packet format - 100% backward compatible with iOS version
* *
@ -61,7 +83,10 @@ data class BitchatPacket(
val payload: ByteArray, val payload: ByteArray,
var signature: ByteArray? = null, // Changed from val to var for packet signing var signature: ByteArray? = null, // Changed from val to var for packet signing
var ttl: UByte, var ttl: UByte,
var route: List<ByteArray>? = null // Optional source route: ordered list of peerIDs (8 bytes each), not including sender and final recipient var route: List<ByteArray>? = null, // Optional source route: ordered list of peerIDs (8 bytes each), not including sender and final recipient
// Set by BinaryProtocol.decode. Not part of packet identity, so it stays out of
// the parcel, equals and hashCode. Losing it only costs a re-compression.
@IgnoredOnParcel val wirePayload: WirePayload? = null
) : Parcelable { ) : Parcelable {
constructor( constructor(
@ -100,7 +125,8 @@ data class BitchatPacket(
payload = payload, payload = payload,
signature = null, // Remove signature for signing signature = null, // Remove signature for signing
route = route, route = route,
ttl = com.bitchat.android.util.AppConstants.SYNC_TTL_HOPS // Use fixed TTL=0 for signing to ensure relay compatibility ttl = com.bitchat.android.util.AppConstants.SYNC_TTL_HOPS, // Use fixed TTL=0 for signing to ensure relay compatibility
wirePayload = wirePayload // preimage must use the originator's bytes
) )
return BinaryProtocol.encode(unsignedPacket) return BinaryProtocol.encode(unsignedPacket)
} }
@ -214,7 +240,17 @@ object BinaryProtocol {
var originalPayloadSize: Int? = null var originalPayloadSize: Int? = null
var isCompressed = false var isCompressed = false
if (CompressionUtil.shouldCompress(payload)) { // Re-encode of a decoded packet: reuse the originator's bytes (see WirePayload).
val wire = packet.wirePayload
if (wire != null && wire.forPayload.contentEquals(packet.payload)) {
if (wire.compressed) {
payload = wire.bytes
originalPayloadSize = packet.payload.size
isCompressed = true
}
// Uncompressed on the wire: keep it that way. shouldCompress agrees
// across clients, but the "only if smaller" check need not.
} else if (CompressionUtil.shouldCompress(payload)) {
CompressionUtil.compress(payload)?.let { compressedPayload -> CompressionUtil.compress(payload)?.let { compressedPayload ->
originalPayloadSize = payload.size originalPayloadSize = payload.size
payload = compressedPayload payload = compressedPayload
@ -450,6 +486,8 @@ object BinaryProtocol {
} else null } else null
// Payload // Payload
// Kept so the packet can be re-encoded byte-identically (see WirePayload).
var receivedCompressed: ByteArray? = null
val payload = if (isCompressed) { val payload = if (isCompressed) {
val lengthFieldBytes = if (version >= 2u.toUByte()) 4 else 2 val lengthFieldBytes = if (version >= 2u.toUByte()) 4 else 2
if (payloadLength.toInt() < lengthFieldBytes) return null if (payloadLength.toInt() < lengthFieldBytes) return null
@ -488,6 +526,8 @@ object BinaryProtocol {
val expandedPayload = CompressionUtil.withDecompressionResources(resourceBytes) { val expandedPayload = CompressionUtil.withDecompressionResources(resourceBytes) {
val compressedPayload = ByteArray(compressedSize) val compressedPayload = ByteArray(compressedSize)
buffer.get(compressedPayload) buffer.get(compressedPayload)
// Captured here so the allocation stays inside the reservation.
receivedCompressed = compressedPayload
decompress(compressedPayload, originalSize) decompress(compressedPayload, originalSize)
} ?: return null } ?: return null
if (expandedPayload.size != originalSize) { if (expandedPayload.size != originalSize) {
@ -520,7 +560,12 @@ object BinaryProtocol {
payload = payload, payload = payload,
signature = signature, signature = signature,
ttl = ttl, ttl = ttl,
route = route route = route,
wirePayload = WirePayload(
bytes = receivedCompressed ?: payload,
compressed = receivedCompressed != null,
forPayload = payload
)
) )
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -317,6 +317,7 @@ class ConversationRepository internal constructor(
true true
} catch (error: Exception) { } catch (error: Exception) {
Log.e(TAG, "Unable to synchronously clear private conversations", error) Log.e(TAG, "Unable to synchronously clear private conversations", error)
database.destroyStorage()
_storeState.value = ConversationStoreState.Error( _storeState.value = ConversationStoreState.Error(
error.message ?: "Unable to erase conversations" error.message ?: "Unable to erase conversations"
) )
@ -1097,6 +1098,22 @@ internal class ConversationDatabase(
writableDatabase.rawQuery("PRAGMA incremental_vacuum", null).use { } writableDatabase.rawQuery("PRAGMA incremental_vacuum", null).use { }
} }
/**
* Last-resort panic cleanup when [clearAll] throws: drop the encrypted database
* files so a later reload cannot resurrect erased conversations (#699).
*/
fun destroyStorage() {
try {
clearAll()
} catch (error: Exception) {
Log.e(TAG, "clearAll failed during destroyStorage; deleting database files", error)
}
try {
close()
} catch (_: Exception) { }
applicationContext.deleteDatabase(databaseName)
}
fun pruneToRetentionLimits(): Set<String> { fun pruneToRetentionLimits(): Set<String> {
val db = writableDatabase val db = writableDatabase
val orphanedMediaPaths = linkedSetOf<String>() val orphanedMediaPaths = linkedSetOf<String>()

View File

@ -1,5 +1,6 @@
package com.bitchat.android.ui package com.bitchat.android.ui
import com.bitchat.android.geohash.ChannelID
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.security.MessageDigest import java.security.MessageDigest
@ -16,7 +17,8 @@ class ChannelManager(
private val state: ChatState, private val state: ChatState,
private val messageManager: MessageManager, private val messageManager: MessageManager,
private val dataManager: DataManager, private val dataManager: DataManager,
private val coroutineScope: CoroutineScope private val coroutineScope: CoroutineScope,
private val onSwitchToMeshLocation: () -> Unit = {}
) { ) {
// Channel encryption and security // Channel encryption and security
@ -109,6 +111,8 @@ class ChannelManager(
fun switchToChannel(channel: String?) { fun switchToChannel(channel: String?) {
state.setCurrentChannel(channel) state.setCurrentChannel(channel)
state.setSelectedPrivateChatPeer(null) state.setSelectedPrivateChatPeer(null)
state.setSelectedLocationChannel(ChannelID.Mesh)
onSwitchToMeshLocation()
// Clear unread count // Clear unread count
channel?.let { ch -> channel?.let { ch ->

View File

@ -140,7 +140,17 @@ class ChatViewModel(
private val conversationListPreferences = private val conversationListPreferences =
com.bitchat.android.services.ConversationListPreferences.getInstance(getApplication()) com.bitchat.android.services.ConversationListPreferences.getInstance(getApplication())
private val messageManager = MessageManager(state) private val messageManager = MessageManager(state)
private val channelManager = ChannelManager(state, messageManager, dataManager, viewModelScope) private val channelManager = ChannelManager(
state,
messageManager,
dataManager,
viewModelScope,
onSwitchToMeshLocation = {
com.bitchat.android.geohash.LocationChannelManager
.getInstance(getApplication())
.select(com.bitchat.android.geohash.ChannelID.Mesh)
}
)
// Create Noise session delegate for clean dependency injection // Create Noise session delegate for clean dependency injection
private val noiseSessionDelegate = object : NoiseSessionDelegate { private val noiseSessionDelegate = object : NoiseSessionDelegate {

View File

@ -942,6 +942,61 @@ class BinaryProtocolTest {
assertTrue("Payload must match", payload.contentEquals(decoded.payload)) assertTrue("Payload must match", payload.contentEquals(decoded.payload))
} }
/**
* Re-encoding preserves a payload compressed by a different DEFLATE encoder
*
* Verification re-encodes the packet, so a re-encode must reproduce the
* originator's bytes. iOS compresses with Apple's compression_encode_buffer,
* which need not match java.util.zip.Deflater byte for byte.
*
* The payload is wrapped in a raw-DEFLATE "stored" block (BTYPE=00): valid, it
* inflates correctly, and no compressor would emit it, so it stands in for a
* foreign encoder without a second compression library. Compared unpadded so
* the result does not depend on how padding is generated.
*/
@Test
fun `re-encoding preserves a foreign encoder's compressed payload`() {
val payload = "the mesh is up near the north gate. relay running all evening. "
.repeat(4)
.toByteArray()
// Raw DEFLATE stored block: BFINAL=1 BTYPE=00, then LEN and NLEN little-endian.
val stored = ByteArray(5 + payload.size)
stored[0] = 0x01
stored[1] = (payload.size and 0xFF).toByte()
stored[2] = ((payload.size shr 8) and 0xFF).toByte()
stored[3] = (payload.size.inv() and 0xFF).toByte()
stored[4] = ((payload.size.inv() shr 8) and 0xFF).toByte()
payload.copyInto(stored, 5)
val buffer = ByteBuffer.allocate(1024).apply { order(ByteOrder.BIG_ENDIAN) }
buffer.put(2.toByte()) // version = 2
buffer.put(MessageType.MESSAGE.value.toByte()) // type
buffer.put(5.toByte()) // ttl
buffer.putLong(fixedTimestamp.toLong()) // timestamp (8 bytes)
buffer.put(BinaryProtocol.Flags.IS_COMPRESSED.toByte()) // flags
buffer.putInt(stored.size + 4) // payloadLength incl. originalSize
buffer.put(hexToBytes(senderHex)) // senderID (8 bytes)
buffer.putInt(payload.size) // originalSize
buffer.put(stored) // compressed payload
val raw = ByteArray(buffer.position())
buffer.rewind()
buffer.get(raw)
val decoded = BinaryProtocol.decode(raw)
assertNotNull("Foreign-encoded frame must decode", decoded)
assertArrayEquals("Payload must inflate to the original", payload, decoded!!.payload)
val reEncoded = BinaryProtocol.encode(decoded, padding = false)
assertNotNull("Re-encode must succeed", reEncoded)
assertArrayEquals(
"Re-encode must reproduce the originator's bytes, or a valid signature stops verifying",
raw,
reEncoded
)
}
/** /**
* v2 compression bomb is rejected * v2 compression bomb is rejected
* *

View File

@ -24,11 +24,12 @@ import org.robolectric.RobolectricTestRunner
import java.util.Date import java.util.Date
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
class CommandProcessorTest() { class CommandProcessorTest {
private val context: Context = ApplicationProvider.getApplicationContext() private val context: Context = ApplicationProvider.getApplicationContext()
@OptIn(ExperimentalCoroutinesApi::class)
private val testDispatcher = UnconfinedTestDispatcher() @OptIn(ExperimentalCoroutinesApi::class)
private val testScope = TestScope(testDispatcher) private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = TestScope(testDispatcher)
private val chatState = ChatState(scope = testScope) private val chatState = ChatState(scope = testScope)
private lateinit var commandProcessor: CommandProcessor private lateinit var commandProcessor: CommandProcessor
@ -63,11 +64,11 @@ class CommandProcessorTest() {
val channel = "channel-1" val channel = "channel-1"
val result = commandProcessor.processCommand( val result = commandProcessor.processCommand(
command = "/j $channel", command = "/j $channel",
meshService = meshService, meshService = meshService,
myPeerID = "peer-id", myPeerID = "peer-id",
onSendMessage = { a, b, c -> { } }, onSendMessage = { a, b, c -> { } },
viewModel = null viewModel = null
) )
assertEquals(result, true) assertEquals(result, true)
@ -163,6 +164,24 @@ class CommandProcessorTest() {
assertEquals(0, chatState.getMessagesValue().size) assertEquals(0, chatState.getMessagesValue().size)
} }
@Test
fun `join command leaves active geohash selection for mesh channel routing`() {
chatState.setSelectedLocationChannel(
ChannelID.Location(GeohashChannel(GeohashChannelLevel.REGION, "9q"))
)
commandProcessor.processCommand(
command = "/join backchannel",
meshService = meshService,
myPeerID = "peer-id",
onSendMessage = { _, _, _ -> },
viewModel = null
)
assertEquals("#backchannel", chatState.getCurrentChannelValue())
assertEquals(ChannelID.Mesh, chatState.selectedLocationChannel.value)
}
@Test @Test
fun `clearSuggestions hides the command suggestion popup`() { fun `clearSuggestions hides the command suggestion popup`() {
// Typing "/" opens the command popup. // Typing "/" opens the command popup.
@ -187,5 +206,6 @@ class CommandProcessorTest() {
commandProcessor.clearSuggestions() commandProcessor.clearSuggestions()
assertFalse(chatState.getShowMentionSuggestionsValue()) assertFalse(chatState.getShowMentionSuggestionsValue())
assertTrue(chatState.getMentionSuggestionsValue().isEmpty()) assertTrue(chatState.getMentionSuggestionsValue().isEmpty())
} }
} }

View File

@ -12,9 +12,38 @@ class IdentityAnnouncementTest {
private val signingKey = ByteArray(32) { 0x22 } private val signingKey = ByteArray(32) { 0x22 }
@Test @Test
fun `private media capability uses iOS little-endian bytes`() { fun `capability assignments and encoding match iOS`() {
assertEquals(1L shl 0, PeerCapabilities.PREKEYS.rawValue)
assertEquals(1L shl 1, PeerCapabilities.WIFI_BULK.rawValue)
assertEquals(1L shl 2, PeerCapabilities.GATEWAY.rawValue)
assertEquals(1L shl 3, PeerCapabilities.GROUPS.rawValue)
assertEquals(1L shl 4, PeerCapabilities.BOARD.rawValue)
assertEquals(1L shl 5, PeerCapabilities.VOUCH.rawValue)
assertEquals(1L shl 6, PeerCapabilities.MESH_DIAGNOSTICS.rawValue)
assertEquals(1L shl 7, PeerCapabilities.BRIDGE.rawValue)
assertEquals(1L shl 8, PeerCapabilities.PRIVATE_MEDIA.rawValue)
assertEquals(1L shl 9, PeerCapabilities.PRIVATE_MEDIA_RECEIPTS.rawValue)
assertEquals(1L shl 10, PeerCapabilities.NON_DESTRUCTIVE_NOISE_REPLACEMENT.rawValue)
val assigned = PeerCapabilities((1L shl 11) - 1)
assertArrayEquals(byteArrayOf(0xFF.toByte(), 0x07), assigned.encoded())
assertEquals(assigned, PeerCapabilities.decode(assigned.encoded()))
}
@Test
fun `capability encoding is minimal and preserves low 64 unknown bits`() {
assertArrayEquals(byteArrayOf(0x00), PeerCapabilities.NONE.encoded())
assertArrayEquals(byteArrayOf(0x01), PeerCapabilities.PREKEYS.encoded())
assertArrayEquals(byteArrayOf(0x00, 0x01), PeerCapabilities.PRIVATE_MEDIA.encoded()) assertArrayEquals(byteArrayOf(0x00, 0x01), PeerCapabilities.PRIVATE_MEDIA.encoded())
assertTrue(PeerCapabilities.decode(byteArrayOf(0x00, 0x01)).contains(PeerCapabilities.PRIVATE_MEDIA))
val future = PeerCapabilities.decode(
byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80.toByte(), 0x55)
)
assertEquals(Long.MIN_VALUE or 1L, future.rawValue)
assertArrayEquals(
byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80.toByte()),
future.encoded()
)
} }
@Test @Test
@ -71,5 +100,6 @@ class IdentityAnnouncementTest {
.capabilities!! .capabilities!!
.contains(PeerCapabilities.PRIVATE_MEDIA) .contains(PeerCapabilities.PRIVATE_MEDIA)
) )
assertEquals(PeerCapabilities.PRIVATE_MEDIA, PeerCapabilities.LOCAL_SUPPORTED)
} }
} }

View File

@ -0,0 +1,136 @@
package com.bitchat.android.nostr
import android.app.Application
import androidx.test.core.app.ApplicationProvider
import com.bitchat.android.ui.ChatState
import com.bitchat.android.ui.DataManager
import com.bitchat.android.ui.MessageManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
/**
* Regression coverage for GH-733: geohash events (kind 20000/20001) were accepted from the
* relay without Schnorr signature verification, allowing full pubkey impersonation.
*/
@RunWith(RobolectricTestRunner::class)
class GeohashMessageHandlerSignatureTest {
private val application: Application = ApplicationProvider.getApplicationContext()
@OptIn(ExperimentalCoroutinesApi::class)
private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = TestScope(testDispatcher)
private lateinit var chatState: ChatState
private lateinit var dataManager: DataManager
private lateinit var messageManager: MessageManager
private lateinit var repo: GeohashRepository
private lateinit var handler: GeohashMessageHandler
private val geohash = "u4pruy"
@Before
fun setup() {
chatState = ChatState(scope = testScope)
dataManager = DataManager(context = application)
messageManager = MessageManager(state = chatState)
repo = GeohashRepository(application, chatState, dataManager)
handler = GeohashMessageHandler(
application = application,
repo = repo,
scope = testScope,
dataManager = dataManager,
addChannelMessage = messageManager::addChannelMessage,
signatureVerificationDispatcher = testDispatcher
)
}
private fun buildSignedEvent(identity: NostrIdentity, content: String): NostrEvent {
val unsigned = NostrEvent(
pubkey = identity.publicKeyHex,
createdAt = (System.currentTimeMillis() / 1000).toInt(),
kind = NostrKind.EPHEMERAL_EVENT,
tags = listOf(listOf("g", geohash)),
content = content
)
return unsigned.sign(identity.privateKeyHex)
}
@Test
fun onEvent_acceptsGenuinelySignedEvent() {
val victim = NostrIdentity.generate()
val genuine = buildSignedEvent(victim, "hello from the real victim")
handler.onEvent(genuine, geohash)
val stored = chatState.getChannelMessagesValue()["geo:$geohash"]
assertEquals(1, stored?.size)
assertEquals("hello from the real victim", stored?.first()?.content)
}
@Test
fun onEvent_rejectsEventWithForgedSignature() {
val victim = NostrIdentity.generate()
val attacker = NostrIdentity.generate()
// Attacker claims the victim's pubkey but signs with their own key - a forged event.
val forged = buildSignedEvent(victim, "impersonated message").copy(
sig = NostrEvent(
pubkey = attacker.publicKeyHex,
createdAt = (System.currentTimeMillis() / 1000).toInt(),
kind = NostrKind.EPHEMERAL_EVENT,
tags = listOf(listOf("g", geohash)),
content = "impersonated message"
).sign(attacker.privateKeyHex).sig
)
// Sanity check: this event is indeed invalid before we even touch the handler.
assertEquals(false, forged.isValidSignature())
handler.onEvent(forged, geohash)
val stored = chatState.getChannelMessagesValue()["geo:$geohash"]
assertNull("Forged event must not be rendered as a legitimate message", stored)
}
@Test
fun onEvent_stillAcceptsGenuineEventAfterForgedCopyWithSameIdWasRejected() {
val victim = NostrIdentity.generate()
val attacker = NostrIdentity.generate()
val genuine = buildSignedEvent(victim, "hello from the real victim")
// A forged event carrying the SAME id as the genuine one (ids are a content hash,
// independent of the signature), but signed by an attacker - relays can deliver this
// before the genuine copy arrives from another relay.
val forgedWithSameId = genuine.copy(
sig = NostrEvent(
pubkey = attacker.publicKeyHex,
createdAt = genuine.createdAt,
kind = NostrKind.EPHEMERAL_EVENT,
tags = listOf(listOf("g", geohash)),
content = "hello from the real victim"
).sign(attacker.privateKeyHex).sig
)
assertEquals(genuine.id, forgedWithSameId.id)
assertEquals(false, forgedWithSameId.isValidSignature())
// Forged copy arrives first and must be rejected without poisoning the dedup cache.
handler.onEvent(forgedWithSameId, geohash)
assertNull(chatState.getChannelMessagesValue()["geo:$geohash"])
// The genuine copy (same id, valid signature) arrives afterwards from another relay -
// it must still be rendered, not silently dropped as a "duplicate".
handler.onEvent(genuine, geohash)
val stored = chatState.getChannelMessagesValue()["geo:$geohash"]
assertEquals(1, stored?.size)
assertEquals("hello from the real victim", stored?.first()?.content)
}
}

View File

@ -36,17 +36,21 @@ class NostrBackgroundEventProcessorTest {
fun `cold start processes more events than the removed handoff queue capacity`() = runBlocking { fun `cold start processes more events than the removed handoff queue capacity`() = runBlocking {
val application = ApplicationProvider.getApplicationContext<Application>() val application = ApplicationProvider.getApplicationContext<Application>()
val processor = NostrBackgroundEventProcessor(application, scope) val processor = NostrBackgroundEventProcessor(application, scope)
val identity = NostrIdentity.generate()
val expectedIds = mutableSetOf<String>()
repeat(300) { index -> repeat(300) { index ->
val event = NostrEvent(
pubkey = identity.publicKeyHex,
createdAt = index + 1,
kind = NostrKind.EPHEMERAL_EVENT,
tags = listOf(listOf("g", "u4pruy")),
content = "message-$index"
).sign(identity.privateKeyHex)
expectedIds += event.id
processor.onGeohashMessage( processor.onGeohashMessage(
event = NostrEvent( event = event,
id = "cold-start-$index",
pubkey = index.toString(16).padStart(64, '0'),
createdAt = 1,
kind = NostrKind.EPHEMERAL_EVENT,
tags = listOf(listOf("g", "u4pruy")),
content = "message-$index"
),
geohash = "u4pruy" geohash = "u4pruy"
) )
} }
@ -58,7 +62,7 @@ class NostrBackgroundEventProcessorTest {
} }
assertEquals( assertEquals(
(0 until 300).map { "cold-start-$it" }.toSet(), expectedIds,
AppStateStore.channelMessages.value["geo:u4pruy"].orEmpty().map { it.id }.toSet() AppStateStore.channelMessages.value["geo:u4pruy"].orEmpty().map { it.id }.toSet()
) )
} }

View File

@ -433,6 +433,22 @@ class ConversationDatabaseTest {
} }
} }
@Test
fun `destroy storage deletes database files so history cannot reload`() {
database.upsertMessage(
"contact_alice",
setOf("contact_alice"),
"alice",
message("survives-failure", "alice", 1L),
true
)
assertTrue(context.databaseList().contains(databaseName))
database.destroyStorage()
assertFalse(context.databaseList().contains(databaseName))
}
@Test @Test
fun `version one plaintext database migrates without losing history`() { fun `version one plaintext database migrates without losing history`() {
database.close() database.close()

View File

@ -29,9 +29,16 @@ Existing TLVs (unchanged):
Other optional extension: Other optional extension:
- `0x05` CAPABILITIES: Minimal little-endian feature bitfield. Bit 8 advertises - `0x05` CAPABILITIES: Minimal little-endian feature bitfield. Assigned bits
authenticated Noise private media (`PRIVATE_MEDIA_V1`); its exact value is match iOS: 0 prekeys, 1 Wi-Fi bulk, 2 gateway, 3 private groups, 4 board,
`00 01`. An empty value is valid and means no advertised capabilities. 5 vouch, 6 mesh diagnostics, 7 bridge, 8 authenticated Noise private media,
9 private-media receipts, and 10 reserved non-destructive Noise replacement.
Bit 8's exact value is `00 01`. An empty value is valid and means no
advertised capabilities. For this announcement TLV, unknown bits and bytes
beyond the low 64 bits must be tolerated for forward compatibility. This
tolerance does not apply to the Noise-authenticated peer-state payload
`0x21`, whose capability field must be canonical and 1...8 bytes; see
`PRIVATE_MEDIA_V1.md`.
New TLV (optional): New TLV (optional):
@ -69,6 +76,12 @@ This matches the onwire 8byte `senderID`/`recipientID` encoding used in th
security-sensitive bit is trusted only after the signed announcement key is security-sensitive bit is trusted only after the signed announcement key is
bound to the matching remote static key from a live Noise handshake; see bound to the matching remote static key from a live Noise handshake; see
`PRIVATE_MEDIA_V1.md`. `PRIVATE_MEDIA_V1.md`.
- Capability bits are independent discovery hints. Implementations advertise
only wire features they implement and must not infer support from platform,
app version, or unrelated transports with similar names.
- Private-media receipts and retry (bit 9) require the current Noise
generation's authenticated `0x21` peer state to prove both bits 8 and 9. An
announcement alone never authorizes that behavior.
- If a `0x04` TLV is present: - If a `0x04` TLV is present:
- Interpret the value as `N = length / 8` peer IDs (ignore trailing nonaligned bytes). - Interpret the value as `N = length / 8` peer IDs (ignore trailing nonaligned bytes).
- Each 8byte chunk is decoded back to a 16hexchar peer ID string (lowercase). - Each 8byte chunk is decoded back to a 16hexchar peer ID string (lowercase).

View File

@ -31,10 +31,15 @@ GitHub/Google Play publication checklist.
The build uses a clean source tree, an isolated Gradle user home, UTC, a stable The build uses a clean source tree, an isolated Gradle user home, UTC, a stable
locale, `SOURCE_DATE_EPOCH` from the Git commit, no Gradle build or configuration locale, `SOURCE_DATE_EPOCH` from the Git commit, no Gradle build or configuration
cache, fresh tasks, and a non-incremental in-process Kotlin compiler. Native cache, fresh tasks, a non-incremental in-process Kotlin compiler, and R8's
builds remap source paths and release validation rejects host paths in packaged deterministic-debugging mode. The R8 mode uses one compiler thread and disables
libraries. The container overlays a canonical `local.properties`, so an ignored randomized input shuffling. The regular R8 ProGuard map remains embedded in each
Android Studio file cannot redirect Gradle to a host-specific SDK. AAB. Kotlin 2.4.10's optional Compose group-key mapping augmentation is disabled
because its duplicate-key selection depends on unspecified class-file iteration
order across clean builds. Native builds remap source paths and release
validation rejects host paths in packaged libraries. The container overlays a
canonical `local.properties`, so an ignored Android Studio file cannot redirect
Gradle to a host-specific SDK.
AGP's embedded VCS record is disabled because its Git discovery depends on the AGP's embedded VCS record is disabled because its Git discovery depends on the
host checkout layout. The canonical `BUILDINFO.json` and GitHub provenance host checkout layout. The canonical `BUILDINFO.json` and GitHub provenance
@ -54,8 +59,10 @@ The authoritative pins are:
## Reproduce a release locally ## Reproduce a release locally
Requirements are Git, Docker with Linux/amd64 support, and enough free space for Requirements are Git, Docker with Linux/amd64 support, more than 8 GiB of memory
the Android and Gradle images and dependencies. available to the Docker VM (16 GiB recommended), and enough free space for the
Android and Gradle images and dependencies. R8's single-threaded deterministic
mode can exceed an 8 GiB Docker memory limit while optimizing the phone app.
```bash ```bash
git clone https://github.com/permissionlesstech/bitchat-android.git git clone https://github.com/permissionlesstech/bitchat-android.git

View File

@ -2147,6 +2147,9 @@
<artifact name="aapt2-9.3.1-15703166-osx.jar"> <artifact name="aapt2-9.3.1-15703166-osx.jar">
<sha256 value="1e35bc2ce18c3aae840be2a29659ce50d6043e907a44d98ee1cf375d044fa29c" origin="Generated by Gradle"/> <sha256 value="1e35bc2ce18c3aae840be2a29659ce50d6043e907a44d98ee1cf375d044fa29c" origin="Generated by Gradle"/>
</artifact> </artifact>
<artifact name="aapt2-9.3.1-15703166-windows.jar">
<sha256 value="b1006ecec7e5936257e95e97f3eba7ef439d3e44178967cc048f86c9119fb231" origin="Generated by Gradle"/>
</artifact>
<artifact name="aapt2-9.3.1-15703166.pom"> <artifact name="aapt2-9.3.1-15703166.pom">
<sha256 value="edb82dcbe738ca78b31612cb307bf99e5f02f25c8125048f933e792fd7778beb" origin="Generated by Gradle"/> <sha256 value="edb82dcbe738ca78b31612cb307bf99e5f02f25c8125048f933e792fd7778beb" origin="Generated by Gradle"/>
</artifact> </artifact>
@ -4911,6 +4914,9 @@
</artifact> </artifact>
</component> </component>
<component group="org.junit" name="junit-bom" version="5.9.2"> <component group="org.junit" name="junit-bom" version="5.9.2">
<artifact name="junit-bom-5.9.2.module">
<sha256 value="ab137ba5a8e32c9b066bf9126a1c76dd5614b724ba5c0b02549772b5e9f4cf1f" origin="Generated by Gradle"/>
</artifact>
<artifact name="junit-bom-5.9.2.pom"> <artifact name="junit-bom-5.9.2.pom">
<sha256 value="2ed07d65845131f5336a86476c9a4056b59d0b58b9815ab3679bb0f36f35f705" origin="Generated by Gradle"/> <sha256 value="2ed07d65845131f5336a86476c9a4056b59d0b58b9815ab3679bb0f36f35f705" origin="Generated by Gradle"/>
</artifact> </artifact>

View File

@ -42,6 +42,9 @@ if ! [[ "$SOURCE_DATE_EPOCH" =~ ^[0-9]+$ ]]; then
fi fi
gradle_args=( gradle_args=(
# R8 otherwise randomizes input traversal and uses multiple compiler threads.
# The optimized DEX can still match while its embedded ProGuard map differs.
-Dcom.android.tools.r8.deterministicdebugging=1
-Pkotlin.compiler.execution.strategy=in-process -Pkotlin.compiler.execution.strategy=in-process
-Pkotlin.incremental=false -Pkotlin.incremental=false
--no-build-cache --no-build-cache

View File

@ -19,8 +19,8 @@ android {
targetSdk = libs.versions.targetSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt()
// Wear releases use a separate high range because Play requires every artifact in // Wear releases use a separate high range because Play requires every artifact in
// one application ID to have a unique version code across all form factors. // one application ID to have a unique version code across all form factors.
versionCode = 1_000_000_001 versionCode = 1_000_000_003
versionName = "0.1.0" versionName = "0.1.2"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true
@ -65,6 +65,13 @@ android {
} }
} }
composeCompiler {
// Kotlin 2.4.10's optional Compose group-key mapping depends on unspecified
// class-file iteration order. Keep the normal R8 mapping, but omit that
// augmentation until its producer is deterministic across clean builds.
includeComposeMappingFile.set(false)
}
// Shared bitchat protocol stack: compiled from :app sources in place (never moved/copied by hand). // Shared bitchat protocol stack: compiled from :app sources in place (never moved/copied by hand).
// AGP's source directory sets no longer support include/exclude filters, so a Sync task // AGP's source directory sets no longer support include/exclude filters, so a Sync task
// materializes a filtered mirror into build/sharedSrc and that directory is added as a source // materializes a filtered mirror into build/sharedSrc and that directory is added as a source

View File

@ -8,7 +8,6 @@ import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -21,13 +20,20 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.changedToUp import androidx.compose.ui.input.pointer.changedToUp
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.TransformingLazyColumn import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
@ -44,6 +50,7 @@ import com.bitchat.watch.ui.theme.ChatVisualTokens
import com.bitchat.watch.ui.theme.LocalBitchatPalette import com.bitchat.watch.ui.theme.LocalBitchatPalette
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlin.math.sign
/** /**
* The shared chat body for global chat and DM threads, following the classic messenger * The shared chat body for global chat and DM threads, following the classic messenger
@ -82,35 +89,34 @@ fun ChatScaffold(
previousCount = messages.size previousCount = messages.size
} }
// One state drives both overlays: visible at the bottom or when scrolling toward it, // Follow intent is changed only by an actual user scroll away from the newest item or by
// hidden when scrolling up into history. The 24px (~12dp) threshold is deliberately // reaching the end again. A new item temporarily makes canScrollForward true before layout;
// small so the controls answer every flick immediately. // treating that transient range change as user intent breaks automatic following.
var atNewest by remember { mutableStateOf(true) } var followNewest by remember { mutableStateOf(true) }
val controlsVisible = remember { mutableStateOf(true) } val controlsVisible = remember { mutableStateOf(true) }
LaunchedEffect(columnState) { LaunchedEffect(columnState) {
var lastPosition = -1 var lastPosition = -1
var scrollIntent = ChatScrollIntentState()
snapshotFlow { snapshotFlow {
val first = columnState.layoutInfo.visibleItems.firstOrNull() val first = columnState.layoutInfo.visibleItems.firstOrNull()
Triple(columnState.canScrollForward, first?.index ?: 0, first?.offset ?: 0) ChatScrollSnapshot(
}.collect { (canScrollForward, index, offset) -> canScrollForward = columnState.canScrollForward,
val position = index * 100_000 + offset isScrollInProgress = columnState.isScrollInProgress,
atNewest = !canScrollForward position = (first?.index ?: 0) * 100_000 + (first?.offset ?: 0)
if (!canScrollForward) { )
controlsVisible.value = true }.collect { snapshot ->
} else if (lastPosition >= 0) { scrollIntent = updatedChatScrollIntent(
when { current = scrollIntent,
position > lastPosition + 24 -> controlsVisible.value = true snapshot = snapshot,
position < lastPosition - 24 -> controlsVisible.value = false previousPosition = lastPosition
} )
} followNewest = scrollIntent.followsNewest
lastPosition = position controlsVisible.value = scrollIntent.controlsVisible
lastPosition = snapshot.position
} }
} }
// Stick to bottom: follow new messages while resting at the newest. // Stick to bottom when the user has not intentionally moved into history.
// Capture this when the message count changes, before the new layout can temporarily make
// canScrollForward true and report that the user is browsing history.
val followNewest = remember(messages.size) { atNewest }
LaunchedEffect(columnState, messages.size) { LaunchedEffect(columnState, messages.size) {
if (messages.isNotEmpty() && followNewest) { if (messages.isNotEmpty() && followNewest) {
val expectedSingleMessageKey = messages.singleOrNull()?.id val expectedSingleMessageKey = messages.singleOrNull()?.id
@ -155,6 +161,50 @@ internal data class MeasuredChatLayout(
val singleVisibleItemKey: Any? val singleVisibleItemKey: Any?
) )
internal data class ChatScrollSnapshot(
val canScrollForward: Boolean,
val isScrollInProgress: Boolean,
val position: Int
)
internal data class ChatScrollIntentState(
val followsNewest: Boolean = true,
val controlsVisible: Boolean = true,
val accumulatedDeltaPx: Int = 0
)
internal fun updatedChatScrollIntent(
current: ChatScrollIntentState,
snapshot: ChatScrollSnapshot,
previousPosition: Int
): ChatScrollIntentState {
if (!snapshot.canScrollForward) return ChatScrollIntentState()
if (!snapshot.isScrollInProgress || previousPosition < 0) return current
val delta = snapshot.position - previousPosition
val accumulatedDelta = when {
delta == 0 -> current.accumulatedDeltaPx
current.accumulatedDeltaPx == 0 ||
current.accumulatedDeltaPx.sign == delta.sign ->
current.accumulatedDeltaPx + delta
else -> delta
}
val movedAway = accumulatedDelta <= -CHAT_SCROLL_DIRECTION_THRESHOLD_PX
val movedTowardNewest = accumulatedDelta >= CHAT_SCROLL_DIRECTION_THRESHOLD_PX
return current.copy(
followsNewest = current.followsNewest && !movedAway,
controlsVisible = when {
movedAway -> false
movedTowardNewest -> true
else -> current.controlsVisible
},
// Keep sub-threshold movement across discrete rotary events. Once intent is clear,
// start a fresh accumulator so reversing direction gets the same threshold treatment.
accumulatedDeltaPx = if (movedAway || movedTowardNewest) 0 else accumulatedDelta
)
}
internal suspend fun scrollToNewestAfterItemsMeasured( internal suspend fun scrollToNewestAfterItemsMeasured(
expectedItemCount: Int, expectedItemCount: Int,
expectedSingleMessageKey: Any?, expectedSingleMessageKey: Any?,
@ -185,7 +235,7 @@ private fun ChatBody(
val palette = LocalBitchatPalette.current val palette = LocalBitchatPalette.current
val context = LocalContext.current val context = LocalContext.current
val transformationSpec = rememberTransformationSpec() val transformationSpec = rememberTransformationSpec()
val isScreenRound = LocalConfiguration.current.isScreenRound
// Slide-to-cancel: while recording, the finger's position is tracked globally; the // Slide-to-cancel: while recording, the finger's position is tracked globally; the
// overlay's mic button reports its bounds and becomes the cancel target when the // overlay's mic button reports its bounds and becomes the cancel target when the
// finger hovers it (with generous slack so the snap engages on approach). // finger hovers it (with generous slack so the snap engages on approach).
@ -256,16 +306,65 @@ private fun ChatBody(
} }
} }
) { ) {
ScreenScaffold(scrollState = columnState) { ScreenScaffold(scrollState = columnState) { scaffoldPadding ->
val layoutDirection = LocalLayoutDirection.current
TransformingLazyColumn( TransformingLazyColumn(
state = columnState, state = columnState,
modifier = Modifier.fillMaxSize(), // Keep the list full-screen and geometrically unclipped. Wear's transformation
// spec curves rows along the round display; a soft destination-alpha mask then
// makes them fully transparent at the physical edges instead of cutting glyphs.
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
compositingStrategy = CompositingStrategy.Offscreen
}
.drawWithCache {
val edgeMask = if (isScreenRound) {
// Fade toward the actual circular contour so glyphs become
// transparent before the panel can crop their left or right edge.
Brush.radialGradient(
0f to Color.Black,
CHAT_ROUND_EDGE_OPAQUE_STOP to Color.Black,
1f to Color.Transparent,
center = Offset(size.width / 2f, size.height / 2f),
radius = size.minDimension / 2f
)
} else {
val topOpaqueStop =
(CHAT_HEADER_EDGE_FADE.toPx() / size.height).coerceIn(0f, 1f)
val bottomOpaqueStop =
(1f - CHAT_ACTION_BAR_EDGE_FADE.toPx() / size.height)
.coerceIn(topOpaqueStop, 1f)
Brush.verticalGradient(
0f to Color.Transparent,
topOpaqueStop to Color.Black,
bottomOpaqueStop to Color.Black,
1f to Color.Transparent
)
}
onDrawWithContent {
drawContent()
drawRect(
brush = edgeMask,
blendMode = BlendMode.DstIn
)
}
},
// Arrangement.Bottom anchors short content to the bottom: the first message // Arrangement.Bottom anchors short content to the bottom: the first message
// starts just above the action bar and new messages push history upward. // starts just above the action bar and new messages push history upward.
// The padding reserves permanent room for the floating header and action // The scroll range reserves resting space for the floating controls while the
// bar; being constant, it never disturbs an in-flight scroll gesture. // full-screen viewport preserves autoscroll and the native transformation focal
// point. Rows may travel behind the overlays only after they have begun the Wear
// edge scale/fade treatment.
verticalArrangement = Arrangement.Bottom, verticalArrangement = Arrangement.Bottom,
contentPadding = PaddingValues(top = 30.dp, bottom = 64.dp) // Keep Wear Material's responsive horizontal inset while replacing its vertical
// inset with the overlay clearances used before the shape fix. This avoids both
// duplicated padding and a shortened list viewport.
contentPadding = scaffoldPadding.withVerticalClearance(
layoutDirection = layoutDirection,
top = CHAT_HEADER_CONTENT_CLEARANCE,
bottom = CHAT_ACTION_BAR_CLEARANCE
)
) { ) {
if (messages.isEmpty()) { if (messages.isEmpty()) {
item { item {
@ -299,7 +398,12 @@ private fun ChatBody(
// The header stays put and shrinks to its dense form instead of disappearing; // The header stays put and shrinks to its dense form instead of disappearing;
// as an overlay its size animation never touches the list's scroll geometry. // as an overlay its size animation never touches the list's scroll geometry.
Box(modifier = Modifier.align(Alignment.TopCenter)) { Box(
modifier = Modifier
.align(Alignment.TopCenter)
.fillMaxWidth(),
contentAlignment = Alignment.TopCenter
) {
header(controlsVisible) header(controlsVisible)
} }
@ -333,6 +437,12 @@ private fun ChatBody(
// Extra finger slack (px, ~28dp at watch density) around the cancel target so the snap // Extra finger slack (px, ~28dp at watch density) around the cancel target so the snap
// engages as the finger approaches, not only on exact contact. // engages as the finger approaches, not only on exact contact.
private const val CANCEL_HOVER_SLANT_PX = 56f private const val CANCEL_HOVER_SLANT_PX = 56f
private const val CHAT_SCROLL_DIRECTION_THRESHOLD_PX = 24
private val CHAT_HEADER_CONTENT_CLEARANCE = 30.dp
private val CHAT_ACTION_BAR_CLEARANCE = 64.dp
private val CHAT_HEADER_EDGE_FADE = 36.dp
private val CHAT_ACTION_BAR_EDGE_FADE = 72.dp
private const val CHAT_ROUND_EDGE_OPAQUE_STOP = 0.78f
// Magnetic zone geometry (px at watch density): the button starts reacting at // Magnetic zone geometry (px at watch density): the button starts reacting at
// MAGNET_OUTER_PX from its center and fully blushes at MAGNET_INNER_PX (~the activation // MAGNET_OUTER_PX from its center and fully blushes at MAGNET_INNER_PX (~the activation
// boundary); it leans toward the finger by up to MAGNET_PULL_PX. // boundary); it leans toward the finger by up to MAGNET_PULL_PX.

View File

@ -67,10 +67,11 @@ fun PeopleScreen(onOpenDm: (String) -> Unit, onEditNickname: () -> Unit) {
) )
} }
ScreenScaffold(scrollState = listState) { ScreenScaffold(scrollState = listState) { scaffoldPadding ->
ScalingLazyColumn( ScalingLazyColumn(
state = listState, state = listState,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize(),
contentPadding = scaffoldPadding
) { ) {
item { item {
ListHeader { ListHeader {

View File

@ -1,7 +1,6 @@
package com.bitchat.watch.ui package com.bitchat.watch.ui
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -15,6 +14,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -48,11 +48,17 @@ fun UserDetailScreen(
val listState = rememberScalingLazyListState() val listState = rememberScalingLazyListState()
val palette = LocalBitchatPalette.current val palette = LocalBitchatPalette.current
ScreenScaffold(scrollState = listState) { ScreenScaffold(scrollState = listState) { scaffoldPadding ->
val layoutDirection = LocalLayoutDirection.current
ScalingLazyColumn( ScalingLazyColumn(
state = listState, state = listState,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 8.dp) contentPadding = scaffoldPadding
.withAdditionalPadding(
layoutDirection = layoutDirection,
horizontal = 10.dp,
vertical = 8.dp
)
) { ) {
item { item {
ListHeader { ListHeader {

View File

@ -2,7 +2,6 @@ package com.bitchat.watch.ui
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -14,6 +13,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -43,11 +43,17 @@ fun VerificationCodeScreen(peerID: String) {
val listState = rememberScalingLazyListState() val listState = rememberScalingLazyListState()
val palette = LocalBitchatPalette.current val palette = LocalBitchatPalette.current
ScreenScaffold(scrollState = listState) { ScreenScaffold(scrollState = listState) { scaffoldPadding ->
val layoutDirection = LocalLayoutDirection.current
ScalingLazyColumn( ScalingLazyColumn(
state = listState, state = listState,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 8.dp) contentPadding = scaffoldPadding
.withAdditionalPadding(
layoutDirection = layoutDirection,
horizontal = 10.dp,
vertical = 8.dp
)
) { ) {
item { item {
ListHeader { ListHeader {

View File

@ -0,0 +1,53 @@
package com.bitchat.watch.ui
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
/**
* Preserve the responsive, shape-aware padding supplied by Wear Material while allowing a
* screen to reserve additional room for its own content or floating controls.
*/
internal fun PaddingValues.withAdditionalPadding(
layoutDirection: LayoutDirection,
horizontal: Dp = 0.dp,
vertical: Dp = 0.dp
): PaddingValues {
return PaddingValues(
start = calculateStartPadding(layoutDirection) + horizontal,
top = calculateTopPadding() + vertical,
end = calculateEndPadding(layoutDirection) + horizontal,
bottom = calculateBottomPadding() + vertical
)
}
/**
* Keep the responsive horizontal inset supplied by Wear Material while replacing its vertical
* padding with screen-owned clearances. This keeps the lazy list full-screen for correct Wear
* transformation and scroll calculations without stacking duplicate vertical insets.
*/
internal fun PaddingValues.withVerticalClearance(
layoutDirection: LayoutDirection,
top: Dp,
bottom: Dp
): PaddingValues {
return PaddingValues(
start = calculateStartPadding(layoutDirection),
top = top,
end = calculateEndPadding(layoutDirection),
bottom = bottom
)
}
private fun PaddingValues.calculateStartPadding(layoutDirection: LayoutDirection): Dp =
when (layoutDirection) {
LayoutDirection.Ltr -> calculateLeftPadding(layoutDirection)
LayoutDirection.Rtl -> calculateRightPadding(layoutDirection)
}
private fun PaddingValues.calculateEndPadding(layoutDirection: LayoutDirection): Dp =
when (layoutDirection) {
LayoutDirection.Ltr -> calculateRightPadding(layoutDirection)
LayoutDirection.Rtl -> calculateLeftPadding(layoutDirection)
}

View File

@ -10,6 +10,102 @@ import org.junit.Test
class ChatAutoScrollTest { class ChatAutoScrollTest {
@Test
fun `new scroll range from appended message keeps follow intent`() {
val updated = updatedChatScrollIntent(
current = ChatScrollIntentState(),
snapshot = ChatScrollSnapshot(
canScrollForward = true,
isScrollInProgress = false,
position = 100
),
previousPosition = 100
)
assertEquals(ChatScrollIntentState(), updated)
}
@Test
fun `user scroll away disables follow until list reaches newest again`() {
val browsingHistory = updatedChatScrollIntent(
current = ChatScrollIntentState(),
snapshot = ChatScrollSnapshot(
canScrollForward = true,
isScrollInProgress = true,
position = 60
),
previousPosition = 100
)
assertFalse(browsingHistory.followsNewest)
assertFalse(browsingHistory.controlsVisible)
val dockedAgain = updatedChatScrollIntent(
current = browsingHistory,
snapshot = ChatScrollSnapshot(
canScrollForward = false,
isScrollInProgress = false,
position = 200
),
previousPosition = 60
)
assertEquals(ChatScrollIntentState(), dockedAgain)
}
@Test
fun `slow scroll away accumulates intent across sub-threshold updates`() {
var state = ChatScrollIntentState()
var previousPosition = 100
listOf(94, 88, 82, 76).forEach { position ->
state = updatedChatScrollIntent(
current = state,
snapshot = ChatScrollSnapshot(
canScrollForward = true,
isScrollInProgress = true,
position = position
),
previousPosition = previousPosition
)
previousPosition = position
state = updatedChatScrollIntent(
current = state,
snapshot = ChatScrollSnapshot(
canScrollForward = true,
isScrollInProgress = false,
position = position
),
previousPosition = previousPosition
)
}
assertFalse(state.followsNewest)
assertFalse(state.controlsVisible)
assertEquals(0, state.accumulatedDeltaPx)
}
@Test
fun `slow scroll toward newest reveals controls without restoring follow early`() {
var state = ChatScrollIntentState(followsNewest = false, controlsVisible = false)
var previousPosition = 60
listOf(66, 72, 78, 84).forEach { position ->
state = updatedChatScrollIntent(
current = state,
snapshot = ChatScrollSnapshot(
canScrollForward = true,
isScrollInProgress = true,
position = position
),
previousPosition = previousPosition
)
previousPosition = position
}
assertFalse(state.followsNewest)
assertEquals(true, state.controlsVisible)
assertEquals(0, state.accumulatedDeltaPx)
}
@Test @Test
fun `newest scroll waits until appended message is measured`() = runTest { fun `newest scroll waits until appended message is measured`() = runTest {
val measuredLayouts = MutableStateFlow(MeasuredChatLayout(3, null)) val measuredLayouts = MutableStateFlow(MeasuredChatLayout(3, null))

View File

@ -0,0 +1,70 @@
package com.bitchat.watch.ui
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import org.junit.Assert.assertEquals
import org.junit.Test
class WearContentPaddingTest {
@Test
fun `additional padding preserves scaffold insets`() {
val resolved = PaddingValues(start = 10.dp, top = 18.dp, end = 12.dp, bottom = 20.dp)
.withAdditionalPadding(
layoutDirection = LayoutDirection.Ltr,
horizontal = 10.dp,
vertical = 8.dp
)
assertEquals(20.dp, resolved.calculateLeftPadding(LayoutDirection.Ltr))
assertEquals(22.dp, resolved.calculateRightPadding(LayoutDirection.Ltr))
assertEquals(26.dp, resolved.calculateTopPadding())
assertEquals(28.dp, resolved.calculateBottomPadding())
}
@Test
fun `vertical clearance preserves responsive width and replaces scaffold height inset`() {
val resolved = PaddingValues(start = 10.dp, top = 20.dp, end = 12.dp, bottom = 20.dp)
.withVerticalClearance(
layoutDirection = LayoutDirection.Ltr,
top = 30.dp,
bottom = 64.dp
)
assertEquals(10.dp, resolved.calculateLeftPadding(LayoutDirection.Ltr))
assertEquals(12.dp, resolved.calculateRightPadding(LayoutDirection.Ltr))
assertEquals(30.dp, resolved.calculateTopPadding())
assertEquals(64.dp, resolved.calculateBottomPadding())
}
@Test
fun `additional padding preserves logical start and end in rtl`() {
val resolved = PaddingValues(start = 10.dp, top = 18.dp, end = 12.dp, bottom = 20.dp)
.withAdditionalPadding(
layoutDirection = LayoutDirection.Rtl,
horizontal = 10.dp,
vertical = 8.dp
)
assertEquals(22.dp, resolved.calculateLeftPadding(LayoutDirection.Rtl))
assertEquals(20.dp, resolved.calculateRightPadding(LayoutDirection.Rtl))
assertEquals(26.dp, resolved.calculateTopPadding())
assertEquals(28.dp, resolved.calculateBottomPadding())
}
@Test
fun `vertical clearance preserves logical start and end in rtl`() {
val resolved = PaddingValues(start = 10.dp, top = 20.dp, end = 12.dp, bottom = 20.dp)
.withVerticalClearance(
layoutDirection = LayoutDirection.Rtl,
top = 30.dp,
bottom = 64.dp
)
assertEquals(12.dp, resolved.calculateLeftPadding(LayoutDirection.Rtl))
assertEquals(10.dp, resolved.calculateRightPadding(LayoutDirection.Rtl))
assertEquals(30.dp, resolved.calculateTopPadding())
assertEquals(64.dp, resolved.calculateBottomPadding())
}
}