From 80ece7c40fa8724c2fed5a037241e8e09d657ba8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 01:17:18 +0000 Subject: [PATCH] refactor(nip59): replace wrap PoW params with a template-conversion hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GiftWrapEvent.create no longer takes powDifficulty/powIsActive — it takes a single templateConversion hook ((template, ephemeralPubKey) -> template, default identity) that runs on the finished wrap template right before the ephemeral key signs it. The hook receives the ephemeral pubkey because the NIP-01 id a nonce commits to includes it and the key never leaves create(). NIP17Factory forwards the same hook through wrapSeal/createWraps and the create*NIP17 entry points, so quartz's NIP-59/NIP-17 code no longer imports the NIP-13 miner at all; Account builds the mining closure at the call site. Any future pre-sign wrap adjustment flows through the same seam. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V --- .../vitorpamplona/amethyst/model/Account.kt | 10 +++++- .../quartz/nip17Dm/NIP17Factory.kt | 36 ++++++++----------- .../nip59Giftwrap/wraps/GiftWrapEvent.kt | 29 ++++++++------- 3 files changed, 40 insertions(+), 35 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 7e8d3072e7..b996f4a634 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -189,6 +189,7 @@ import com.vitorpamplona.quartz.nip10Notes.content.findHashtags import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris import com.vitorpamplona.quartz.nip10Notes.content.findURLs import com.vitorpamplona.quartz.nip10Notes.threadRootIdOrSelf +import com.vitorpamplona.quartz.nip13Pow.miner.PoWMiner import com.vitorpamplona.quartz.nip13Pow.signer.PoWNostrSigner import com.vitorpamplona.quartz.nip17Dm.NIP17Factory import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent @@ -253,6 +254,7 @@ import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapTemplateConversion import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo @@ -927,7 +929,13 @@ class Account( difficulty = difficulty, persistAs = record, mine = { isActive -> - seals.map { NIP17Factory().wrapSeal(it, expirationDelta, powDifficulty = difficulty, powIsActive = isActive) } + // the wrap's ephemeral key is generated inside the wrap build; + // the conversion hook hands its pubkey back so the nonce can + // commit to it. + val mineWrap: GiftWrapTemplateConversion = { template, ephemeralPubKey -> + PoWMiner.run(template, ephemeralPubKey, difficulty, isActive) + } + seals.map { NIP17Factory().wrapSeal(it, expirationDelta, templateConversion = mineWrap) } }, publish = { wraps -> broadcastPrivately(wraps) }, ) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt index ae9e90d60a..5b8284f329 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt @@ -37,6 +37,7 @@ import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip46RemoteSigner.signer.NostrSignerRemote import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapTemplateConversion import com.vitorpamplona.quartz.utils.mapNotNullAsync import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit @@ -102,24 +103,23 @@ class NIP17Factory { } /** - * Phase two: wraps one pre-signed seal in its ephemeral-key envelope, - * optionally mining a NIP-13 proof of work into the wrap. Local-only — - * no user signer involved. + * Phase two: wraps one pre-signed seal in its ephemeral-key envelope. + * Local-only — no user signer involved. [templateConversion] is the + * pre-sign hook on the wrap template (e.g. NIP-13 mining); see + * [GiftWrapTemplateConversion]. */ fun wrapSeal( addressed: AddressedSeal, expirationDelta: Long?, recipientRelayHint: NormalizedRelayUrl? = null, - powDifficulty: Int? = null, - powIsActive: () -> Boolean = { true }, + templateConversion: GiftWrapTemplateConversion = { template, _ -> template }, ): GiftWrapEvent = GiftWrapEvent.create( event = addressed.seal, recipientPubKey = addressed.recipient, expirationDelta = expirationDelta, recipientRelayHint = recipientRelayHint, - powDifficulty = powDifficulty, - powIsActive = powIsActive, + templateConversion = templateConversion, ) /** @@ -150,8 +150,7 @@ class NIP17Factory { to: Set, signer: NostrSigner, recipientRelayHints: (HexKey) -> NormalizedRelayUrl? = { null }, - wrapPowDifficulty: Int? = null, - wrapPowIsActive: () -> Boolean = { true }, + wrapTemplateConversion: GiftWrapTemplateConversion = { template, _ -> template }, ): List { val innerExpDelta = event.expiration()?.let { @@ -179,8 +178,7 @@ class NIP17Factory { recipientPubKey = next, expirationDelta = innerExpDelta, recipientRelayHint = recipientRelayHints(next), - powDifficulty = wrapPowDifficulty, - powIsActive = wrapPowIsActive, + templateConversion = wrapTemplateConversion, ) } bunkerLimiter?.withPermit { build() } ?: build() @@ -202,11 +200,10 @@ class NIP17Factory { template: EventTemplate, signer: NostrSigner, recipientRelayHints: (HexKey) -> NormalizedRelayUrl? = { null }, - wrapPowDifficulty: Int? = null, - wrapPowIsActive: () -> Boolean = { true }, + wrapTemplateConversion: GiftWrapTemplateConversion = { template2, _ -> template2 }, ): Result { val senderMessage = signer.sign(template) - val wraps = createWraps(senderMessage, senderMessage.groupMembers(), signer, recipientRelayHints, wrapPowDifficulty, wrapPowIsActive) + val wraps = createWraps(senderMessage, senderMessage.groupMembers(), signer, recipientRelayHints, wrapTemplateConversion) return Result( msg = senderMessage, wraps = wraps, @@ -223,8 +220,7 @@ class NIP17Factory { suspend fun createNoteNIP17( template: EventTemplate, signer: NostrSigner, - wrapPowDifficulty: Int? = null, - wrapPowIsActive: () -> Boolean = { true }, + wrapTemplateConversion: GiftWrapTemplateConversion = { template2, _ -> template2 }, ): Result { val senderNote = signer.sign(template) val wraps = @@ -232,8 +228,7 @@ class NIP17Factory { senderNote, senderNote.taggedUserIds().plus(signer.pubKey).toSet(), signer, - wrapPowDifficulty = wrapPowDifficulty, - wrapPowIsActive = wrapPowIsActive, + wrapTemplateConversion = wrapTemplateConversion, ) return Result( msg = senderNote, @@ -245,11 +240,10 @@ class NIP17Factory { template: EventTemplate, signer: NostrSigner, recipientRelayHints: (HexKey) -> NormalizedRelayUrl? = { null }, - wrapPowDifficulty: Int? = null, - wrapPowIsActive: () -> Boolean = { true }, + wrapTemplateConversion: GiftWrapTemplateConversion = { template2, _ -> template2 }, ): Result { val senderMessage = signer.sign(template) - val wraps = createWraps(senderMessage, senderMessage.groupMembers(), signer, recipientRelayHints, wrapPowDifficulty, wrapPowIsActive) + val wraps = createWraps(senderMessage, senderMessage.groupMembers(), signer, recipientRelayHints, wrapTemplateConversion) return Result( msg = senderMessage, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt index 017a4e7f2b..f55abd6850 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt @@ -30,13 +30,21 @@ import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip13Pow.miner.PoWMiner import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri import com.vitorpamplona.quartz.nip40Expiration.ExpirationTag import com.vitorpamplona.quartz.nip59Giftwrap.HasInnerEvent import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils +/** + * Caller hook to adjust the finished wrap template right before the ephemeral + * key signs it — e.g. mining a NIP-13 proof of work into it. Receives the + * ephemeral key's pubkey because the NIP-01 id (what a nonce commits to) + * includes it, and the key never leaves [GiftWrapEvent.create]. Must return a + * template of the same kind; the default is the identity. + */ +typealias GiftWrapTemplateConversion = (template: EventTemplate, ephemeralPubKey: HexKey) -> EventTemplate + @Immutable open class GiftWrapEvent( id: HexKey, @@ -109,10 +117,11 @@ open class GiftWrapEvent( * [recipientRelayHint] — `null` (the default) preserves the * historical 2-element `["p", pubkey]` shape. * - * [powDifficulty] mines a NIP-13 proof of work into the wrap itself - * (the ephemeral-key envelope, never the inner seal or rumor) so DM - * relays can PoW-filter inbox spam. [powIsActive] is the cooperative - * cancellation hook forwarded to the miner. + * [templateConversion] runs on the finished template right before the + * ephemeral key signs it. This is how a caller mines a NIP-13 proof + * of work into the wrap itself (the ephemeral-key envelope, never the + * inner seal or rumor) so DM relays can PoW-filter inbox spam — + * without this NIP-59 code knowing anything about mining. */ fun create( event: Event, @@ -120,8 +129,7 @@ open class GiftWrapEvent( expirationDelta: Long? = null, createdAt: Long = TimeUtils.randomWithTwoDays(), recipientRelayHint: NormalizedRelayUrl? = null, - powDifficulty: Int? = null, - powIsActive: () -> Boolean = { true }, + templateConversion: GiftWrapTemplateConversion = { template, _ -> template }, ): GiftWrapEvent { val signer = NostrSignerSync(KeyPair()) // GiftWrap is always a random key @@ -145,12 +153,7 @@ open class GiftWrapEvent( content = signer.nip44Encrypt(event.toJson(), recipientPubKey), ) - val readyToSign = - if (powDifficulty != null && powDifficulty > 0) { - PoWMiner.run(template, signer.pubKey, powDifficulty, powIsActive) - } else { - template - } + val readyToSign = templateConversion(template, signer.pubKey) return signer.sign( createdAt = readyToSign.createdAt,