From acfbacf78fefb6664a523111f2c8c73a2e85b67f Mon Sep 17 00:00:00 2001 From: wollow Date: Tue, 11 Aug 2026 22:37:44 +0300 Subject: [PATCH] fix(nostr): reserve two-hour DM timestamp margin --- app/src/main/java/com/bitchat/android/nostr/NostrCrypto.kt | 4 ++-- .../main/java/com/bitchat/android/nostr/NostrProtocol.kt | 2 +- .../kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt | 4 ++-- docs/client-rewrite-contracts.md | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrCrypto.kt b/app/src/main/java/com/bitchat/android/nostr/NostrCrypto.kt index cab41ad0..1bbdc972 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrCrypto.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrCrypto.kt @@ -22,7 +22,7 @@ import java.math.BigInteger */ object NostrCrypto { - internal const val NIP17_DEFAULT_MAX_PAST_SECONDS = 85_500 + internal const val NIP17_DEFAULT_MAX_PAST_SECONDS = 79_200 private val secureRandom = SecureRandom() // NIP-44 v2 only @@ -319,7 +319,7 @@ object NostrCrypto { } /** - * Random timestamp in the past, defaulting to 23h45m to leave slack inside iOS's 24h lookback. + * Random timestamp in the past, defaulting to 22 hours to leave 2 hours of slack inside iOS's 24-hour lookback. */ fun randomizeTimestampUpToPast(maxPastSeconds: Int = NIP17_DEFAULT_MAX_PAST_SECONDS): Int { val now = (System.currentTimeMillis() / 1000).toInt() diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt b/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt index 42caefec..7463a378 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt @@ -37,7 +37,7 @@ object NostrProtocol { val rumorId = rumorBase.computeEventIdHex() val rumor = rumorBase.copy(id = rumorId) - // 2. Seal the rumor with 15 minutes of slack inside iOS's 24-hour lookback. + // 2. Seal the rumor with 2 hours of slack inside iOS's 24-hour lookback. val sealedEvent = createSeal( rumor = rumor, recipientPubkey = recipientPubkey, diff --git a/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt b/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt index 29a41f6e..5faae233 100644 --- a/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt @@ -79,7 +79,7 @@ class NostrProtocolTest { afterCreation: Int ) { assertTrue( - "$envelope timestamp must leave 15 minutes inside the iOS lookback", + "$envelope timestamp must leave 2 hours inside the iOS lookback", createdAt >= beforeCreation - MAX_OUTBOUND_BACKDATE_SECONDS ) assertTrue( @@ -132,7 +132,7 @@ class NostrProtocolTest { private companion object { const val IOS_DM_LOOKBACK_SECONDS = 86_400 - const val TIMESTAMP_SAFETY_SLACK_SECONDS = 900 + const val TIMESTAMP_SAFETY_SLACK_SECONDS = 7_200 const val MAX_OUTBOUND_BACKDATE_SECONDS = IOS_DM_LOOKBACK_SECONDS - TIMESTAMP_SAFETY_SLACK_SECONDS } diff --git a/docs/client-rewrite-contracts.md b/docs/client-rewrite-contracts.md index ed92ab79..05d5e1ad 100644 --- a/docs/client-rewrite-contracts.md +++ b/docs/client-rewrite-contracts.md @@ -17,7 +17,7 @@ The remaining implementation work and milestone progress are tracked in | Inner payloads | Noise type bytes, private-message TLVs, peer-state TLVs, file-transfer TLVs, live-voice bursts, fragment header, sync request TLVs | `ClientRewriteWireContractTest`, `AuthenticatedPeerStateTest`, `PrivateMediaTransferPreparerTest`, `VoiceBurstPacketTest`, `FragmentManagerTest` | | Identity/security | Announcement extensions, capability bitfield endianness, Noise static-key binding, handshake identity binding, signatures | `IdentityAnnouncementTest`, `NoiseSessionManagerIdentityBindingTest`, `ClientRewritePrimitiveContractTest` | | Sync/routing | Stable packet IDs, GCS bitstream, replay collapse, TTL handling, relay choice, confirmed graph edges | `ClientRewritePrimitiveContractTest`, `GCSFilterTest`, `PacketRelayManagerTest`, `MeshGraphServiceTest`, `TransportBridgeServiceTest` | -| Nostr | Bech32, secp256k1 key derivation, NIP-01 event IDs/signatures, NIP-44 authenticated encryption, NIP-13 PoW, authenticated NIP-17 seals, 23h45m outbound envelope randomization | `ClientRewriteNostrContractTest`, `NostrProtocolTest` | +| Nostr | Bech32, secp256k1 key derivation, NIP-01 event IDs/signatures, NIP-44 authenticated encryption, NIP-13 PoW, authenticated NIP-17 seals, 22h outbound envelope randomization | `ClientRewriteNostrContractTest`, `NostrProtocolTest` | | Application state | Peer unions, canonical private conversations, chronological history, delivery/read behavior, media migration policy | `AppStateStoreTest`, `PrivateChatManagerTest`, `MediaSendingManagerMigrationTest` | ## Golden-vector policy @@ -31,9 +31,9 @@ Round-trip tests remain useful but are not sufficient on their own: an encoder and decoder can share the same defect. Each critical wire format therefore has at least one literal vector. -NIP-17 receivers must use a lookback at least as long as the maximum timestamp +NIP-17 receivers should reserve safety slack beyond the maximum timestamp randomization used by senders. Android caps outbound seal and gift-wrap -randomization at 23h45m, leaving 15 minutes of slack inside iOS's 24-hour +randomization at 22h, leaving 2 hours of slack inside iOS's 24-hour subscription window, while retaining its 48-hour receive lookback. ## Rewrite acceptance gate