refactor(nip59): replace wrap PoW params with a template-conversion hook

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
This commit is contained in:
Claude
2026-07-11 01:17:18 +00:00
parent 6dd1e4d31d
commit 80ece7c40f
3 changed files with 40 additions and 35 deletions
@@ -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) },
)
@@ -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<HexKey>,
signer: NostrSigner,
recipientRelayHints: (HexKey) -> NormalizedRelayUrl? = { null },
wrapPowDifficulty: Int? = null,
wrapPowIsActive: () -> Boolean = { true },
wrapTemplateConversion: GiftWrapTemplateConversion = { template, _ -> template },
): List<GiftWrapEvent> {
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<ChatMessageEvent>,
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<TextNoteEvent>,
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<ChatMessageEncryptedFileHeaderEvent>,
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,
@@ -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<GiftWrapEvent>, ephemeralPubKey: HexKey) -> EventTemplate<GiftWrapEvent>
@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,