Migrate Nostr publishing off account.prvkey → use resolve_signer (pre-cascade prerequisite for lnbits#17) #23

Closed
opened 2026-05-26 18:38:04 +00:00 by padreug · 1 comment
Owner

Problem

nostr_hooks.py reads account.prvkey directly to sign event-related Nostr publications:

# nostr_hooks.py:18-36
"""Pulls the wallet owner's pubkey/prvkey to sign with the user's identity."""
...
if not account or not account.pubkey or not account.prvkey:
    return None
...
nostr_client, event, account.pubkey, account.prvkey, delete=delete

aiolabs/lnbits#17 (signer abstraction phase 1, PR open) ships an m002 classify job that fail-closed NULLs the accounts.prvkey column for every row at startup. When that PR cascades to a host running this extension, all account.prvkey reads return None and Nostr publishing silently stops.

Umbrella audit at aiolabs/lnbits#21 identifies this extension among 5 affected.

Sites to migrate

File:line Site
nostr_hooks.py:18-36 _resolve_keys-style helper → return NostrSigner instead of (pubkey, prvkey)
Wherever publish_event(client, event, pubkey, prvkey, delete=…) is defined Accept signer: NostrSigner instead of (pubkey, prvkey)

Migration pattern

Replace the keypair helper with a signer-returning one:

from lnbits.core.signers import resolve_signer, NostrSigner
from lnbits.core.signers.base import SignerError

async def _resolve_signer(wallet_id: str) -> NostrSigner | None:
    wallet_obj = await get_wallet(wallet_id)
    if not wallet_obj:
        return None
    account = await get_account(wallet_obj.user)
    if not account or not account.pubkey:
        return None
    try:
        signer = resolve_signer(account)
    except SignerError as exc:
        logger.warning(f"[EVENTS] cannot resolve signer for {account.id[:8]}: {exc}")
        return None
    if not signer.can_sign():
        logger.debug(f"[EVENTS] account {account.id[:8]} cannot sign server-side; skipping publish")
        return None
    return signer

Update the publisher to call signer.sign_event(unsigned_dict) instead of doing its own Schnorr sign with coincurve.PrivateKey(bytes.fromhex(prvkey)). The signer transparently handles LocalSigner (decrypted envelope), ClientSideOnlySigner (raises SignerUnavailableError), and (phase 2.3+) RemoteBunkerSigner.

Acceptance

  • keypair helper replaced with _resolve_signer returning NostrSigner | None
  • publish_event (or equivalent) accepts a NostrSigner instead of (pubkey, prvkey)
  • Extension-local Schnorr signing code removed (signer handles it)
  • Re-grep events/ post-migration: zero account.prvkey references
  • Manual smoke: create an event on a dev instance, verify the Nostr publication round-trips
  • Bump pyproject.toml version to next vX.Y.Z-aio.N per the fork-versioning convention (this extension is already on v1.3.0-aio.2 per the catalog — next would be v1.3.0-aio.3)
  • Add catalog entry to aiolabs/lnbits-extensions/extensions.json (new entry alongside the previous, don't overwrite — operators on older versions need the previous to still resolve)

Timing

Blocks aiolabs/lnbits#17's cascade to any host running the events extension. Events is actively used on aio-demo; this is the highest-priority of the three new issues filed today (restaurant + tasks + events) because it has the closest production exposure.

Cross-references

  • aiolabs/lnbits#21 — umbrella audit (this is one of 5 affected extensions)
  • aiolabs/lnbits#17 — the cascading PR whose m002 NULLs accounts.prvkey
  • aiolabs/lnbits#9 — parent: operator-IdP framing, signer abstraction
  • aiolabs/restaurant#<TBD> + aiolabs/tasks#<TBD> — sister migrations (filed in parallel)
  • ~/dev/coordination/log.md 2026-05-26T19:30Z — cross-session coordination entry
## Problem `nostr_hooks.py` reads `account.prvkey` directly to sign event-related Nostr publications: ```python # nostr_hooks.py:18-36 """Pulls the wallet owner's pubkey/prvkey to sign with the user's identity.""" ... if not account or not account.pubkey or not account.prvkey: return None ... nostr_client, event, account.pubkey, account.prvkey, delete=delete ``` `aiolabs/lnbits#17` (signer abstraction phase 1, PR open) ships an `m002` classify job that **fail-closed NULLs the `accounts.prvkey` column** for every row at startup. When that PR cascades to a host running this extension, all `account.prvkey` reads return `None` and Nostr publishing silently stops. Umbrella audit at `aiolabs/lnbits#21` identifies this extension among 5 affected. ## Sites to migrate | File:line | Site | |---|---| | `nostr_hooks.py:18-36` | `_resolve_keys`-style helper → return `NostrSigner` instead of `(pubkey, prvkey)` | | Wherever `publish_event(client, event, pubkey, prvkey, delete=…)` is defined | Accept `signer: NostrSigner` instead of `(pubkey, prvkey)` | ## Migration pattern Replace the keypair helper with a signer-returning one: ```python from lnbits.core.signers import resolve_signer, NostrSigner from lnbits.core.signers.base import SignerError async def _resolve_signer(wallet_id: str) -> NostrSigner | None: wallet_obj = await get_wallet(wallet_id) if not wallet_obj: return None account = await get_account(wallet_obj.user) if not account or not account.pubkey: return None try: signer = resolve_signer(account) except SignerError as exc: logger.warning(f"[EVENTS] cannot resolve signer for {account.id[:8]}: {exc}") return None if not signer.can_sign(): logger.debug(f"[EVENTS] account {account.id[:8]} cannot sign server-side; skipping publish") return None return signer ``` Update the publisher to call `signer.sign_event(unsigned_dict)` instead of doing its own Schnorr sign with `coincurve.PrivateKey(bytes.fromhex(prvkey))`. The signer transparently handles `LocalSigner` (decrypted envelope), `ClientSideOnlySigner` (raises `SignerUnavailableError`), and (phase 2.3+) `RemoteBunkerSigner`. ## Acceptance - [ ] keypair helper replaced with `_resolve_signer` returning `NostrSigner | None` - [ ] `publish_event` (or equivalent) accepts a `NostrSigner` instead of `(pubkey, prvkey)` - [ ] Extension-local Schnorr signing code removed (signer handles it) - [ ] Re-grep `events/` post-migration: zero `account.prvkey` references - [ ] Manual smoke: create an event on a dev instance, verify the Nostr publication round-trips - [ ] Bump `pyproject.toml` version to next `vX.Y.Z-aio.N` per the fork-versioning convention (this extension is already on `v1.3.0-aio.2` per the catalog — next would be `v1.3.0-aio.3`) - [ ] Add catalog entry to `aiolabs/lnbits-extensions/extensions.json` (new entry alongside the previous, don't overwrite — operators on older versions need the previous to still resolve) ## Timing Blocks `aiolabs/lnbits#17`'s cascade to any host running the events extension. Events is actively used on `aio-demo`; this is the highest-priority of the three new issues filed today (restaurant + tasks + events) because it has the closest production exposure. ## Cross-references - `aiolabs/lnbits#21` — umbrella audit (this is one of 5 affected extensions) - `aiolabs/lnbits#17` — the cascading PR whose m002 NULLs `accounts.prvkey` - `aiolabs/lnbits#9` — parent: operator-IdP framing, signer abstraction - `aiolabs/restaurant#<TBD>` + `aiolabs/tasks#<TBD>` — sister migrations (filed in parallel) - `~/dev/coordination/log.md` 2026-05-26T19:30Z — cross-session coordination entry
Author
Owner

Update 2026-05-27 — async sign_event API confirmed via aiolabs/lnbits#24

NostrSigner.sign_event is migrating to async def in aiolabs/lnbits#24 (open, stacked on PR #19). When that lands, the canonical migration pattern becomes:

from lnbits.core.signers import resolve_for_wallet

async def _resolve_signer(wallet_id: str) -> NostrSigner | None:
    return await resolve_for_wallet(wallet_id)

# In the publisher:
async def publish_event(nostr_client, event, signer: NostrSigner, delete=False):
    signed = await signer.sign_event(event)   # ← await form
    # ... publish signed event

Two notes:

  1. resolve_for_wallet(wallet_id) (lands in aiolabs/lnbits#23) collapses the entire _resolve_keys block into one call.
  2. await signer.sign_event(event) works for all three concrete signers; sync impls expose async signature for ABC consistency. Zero runtime cost on LocalSigner.

Extension-local Schnorr signing code (coincurve.PrivateKey(bytes.fromhex(prvkey)) + Schnorr sign) can be removed — signer handles it.

Acceptance items remain the same. This is the highest-priority of the three new prvkey-migration issues since events runs on aio-demo; the await form lets you ship migration once the lnbits side cascades.

## Update 2026-05-27 — async sign_event API confirmed via `aiolabs/lnbits#24` `NostrSigner.sign_event` is migrating to `async def` in `aiolabs/lnbits#24` (open, stacked on PR #19). When that lands, the canonical migration pattern becomes: ```python from lnbits.core.signers import resolve_for_wallet async def _resolve_signer(wallet_id: str) -> NostrSigner | None: return await resolve_for_wallet(wallet_id) # In the publisher: async def publish_event(nostr_client, event, signer: NostrSigner, delete=False): signed = await signer.sign_event(event) # ← await form # ... publish signed event ``` Two notes: 1. **`resolve_for_wallet(wallet_id)`** (lands in `aiolabs/lnbits#23`) collapses the entire `_resolve_keys` block into one call. 2. **`await signer.sign_event(event)`** works for all three concrete signers; sync impls expose async signature for ABC consistency. Zero runtime cost on `LocalSigner`. Extension-local Schnorr signing code (`coincurve.PrivateKey(bytes.fromhex(prvkey))` + Schnorr sign) can be removed — signer handles it. Acceptance items remain the same. **This is the highest-priority of the three new prvkey-migration issues** since events runs on `aio-demo`; the await form lets you ship migration once the lnbits side cascades.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/events#23
No description provided.