mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
fix(quartz): NostrServer.ingest verifies inline when the queue hook is off
ingest() bypasses the per-connection policy chain, which is where VerifyPolicy lives. The IngestQueue verify hook only exists when parallelVerify is true, so with parallelVerify = false and skipVerify = false, ingest() previously verified nothing — an untrusted mirror upstream on a relay running the legacy in-policy verify path could inject forgeries. ingest() now verifies inline in that configuration (same rejection reason as the queue), so the documented "default keeps verify-everything semantics" holds regardless of parallelVerify. KDoc also spells out that ingest() skips the entire policy chain (blacklists, size limits), which callers must screen for themselves. Test covers the parallelVerify = false server: forged rejected, trusted skip and valid still land. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TtDNpayEYvJH7QuPswND3A
This commit is contained in:
+21
-8
@@ -65,7 +65,7 @@ class NostrServer(
|
||||
private val store: IEventStore,
|
||||
policyBuilder: () -> IRelayPolicy = { VerifyPolicy },
|
||||
parentContext: CoroutineContext = SupervisorJob(),
|
||||
parallelVerify: Boolean = false,
|
||||
private val parallelVerify: Boolean = false,
|
||||
negentropySettings: NegentropySettings = NegentropySettings.Default,
|
||||
listener: RelayServerListener = RelayServerListener.None,
|
||||
limits: RelayLimits? = null,
|
||||
@@ -105,19 +105,32 @@ class NostrServer(
|
||||
* connection — e.g. a mirror worker streaming a trusted upstream
|
||||
* relay, or an import job. Routes through the same group-commit
|
||||
* [IngestQueue] and live fanout as a client EVENT publish, but skips
|
||||
* the per-connection policy chain (there is no connection).
|
||||
* the **entire** per-connection policy chain (there is no
|
||||
* connection): no [VerifyPolicy], no allow/deny lists, no size
|
||||
* limits. Callers own that screening — scope what may enter this
|
||||
* path (e.g. geode's per-upstream mirror filters) accordingly.
|
||||
*
|
||||
* [skipVerify] exempts the event from the parallel signature-verify
|
||||
* hook — the relay-to-relay trust model: pass `true` only for events
|
||||
* from an explicitly configured upstream that already verified them
|
||||
* (Schnorr verify profiles at ~8% of busy ingest CPU). The default
|
||||
* `false` keeps verify-everything semantics.
|
||||
* [skipVerify] exempts the event from signature verification — the
|
||||
* relay-to-relay trust model: pass `true` only for events from an
|
||||
* explicitly configured upstream that already verified them (Schnorr
|
||||
* verify profiles at ~8% of busy ingest CPU). The default `false`
|
||||
* keeps verify-everything semantics regardless of configuration:
|
||||
* when the [IngestQueue] hook is on ([parallelVerify]) it verifies
|
||||
* there, otherwise this method verifies inline — without this, a
|
||||
* server whose verification lives in the (bypassed) policy chain
|
||||
* would silently ingest forgeries.
|
||||
*/
|
||||
suspend fun ingest(
|
||||
event: Event,
|
||||
skipVerify: Boolean = false,
|
||||
onComplete: (IEventStore.InsertOutcome) -> Unit,
|
||||
) = liveStore.submit(event, skipVerify, onComplete)
|
||||
) {
|
||||
if (!skipVerify && !parallelVerify && !event.verify()) {
|
||||
onComplete(IEventStore.InsertOutcome.Rejected("invalid: bad signature or id"))
|
||||
return
|
||||
}
|
||||
liveStore.submit(event, skipVerify, onComplete)
|
||||
}
|
||||
|
||||
init {
|
||||
// Deferred-FTS catch-up worker: tokenizes in the gaps between
|
||||
|
||||
+31
-2
@@ -59,12 +59,15 @@ class NostrServerIngestTest {
|
||||
sig = "f".repeat(128),
|
||||
)
|
||||
|
||||
private fun createServer(dispatcher: CoroutineDispatcher): NostrServer =
|
||||
private fun createServer(
|
||||
dispatcher: CoroutineDispatcher,
|
||||
parallelVerify: Boolean = true,
|
||||
): NostrServer =
|
||||
NostrServer(
|
||||
store = EventStore(null),
|
||||
policyBuilder = { EmptyPolicy },
|
||||
parentContext = dispatcher,
|
||||
parallelVerify = true,
|
||||
parallelVerify = parallelVerify,
|
||||
)
|
||||
|
||||
private suspend fun NostrServer.ingestOutcome(
|
||||
@@ -118,6 +121,32 @@ class NostrServerIngestTest {
|
||||
server.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun forgedEventIsRejectedEvenWithoutTheQueueVerifyHook() =
|
||||
runTest {
|
||||
// With parallelVerify = false the IngestQueue has no verify
|
||||
// hook AND ingest() bypasses the policy chain where
|
||||
// VerifyPolicy would live — the inline fallback is the only
|
||||
// thing standing between an untrusted mirror and forgeries.
|
||||
val server = createServer(UnconfinedTestDispatcher(testScheduler), parallelVerify = false)
|
||||
|
||||
val outcome = server.ingestOutcome(forgedEvent(), skipVerify = false)
|
||||
assertTrue(outcome is IEventStore.InsertOutcome.Rejected)
|
||||
assertTrue(outcome.reason.contains("signature"))
|
||||
|
||||
// The trust switch and valid events still work on this config.
|
||||
assertEquals(
|
||||
IEventStore.InsertOutcome.Accepted,
|
||||
server.ingestOutcome(forgedEvent(2), skipVerify = true),
|
||||
)
|
||||
assertEquals(
|
||||
IEventStore.InsertOutcome.Accepted,
|
||||
server.ingestOutcome(signedEvent(), skipVerify = false),
|
||||
)
|
||||
|
||||
server.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun trustIsPerSubmissionNotPerQueue() =
|
||||
runTest {
|
||||
|
||||
Reference in New Issue
Block a user