PHPackages                             innis/nostr-core - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. innis/nostr-core

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

innis/nostr-core
================

Core domain entities and services for Nostr protocol implementation

v0.3.10(3w ago)155↓76.7%2MITPHPPHP ^8.3

Since Mar 23Pushed 3w agoCompare

[ Source](https://github.com/johninnis/nostr-core)[ Packagist](https://packagist.org/packages/innis/nostr-core)[ RSS](/packages/innis-nostr-core/feed)WikiDiscussions master Synced 3w ago

READMEChangelogDependencies (18)Versions (29)Used By (2)

Nostr Core Package
==================

[](#nostr-core-package)

A PHP library implementing core domain entities and services for the Nostr protocol, built with Clean Architecture principles.

Why this library?
-----------------

[](#why-this-library)

Existing PHP Nostr libraries (nostriphant, swentel/nostr-php) are organised around individual NIPs, mixing protocol concerns, infrastructure, and application logic together. This makes them difficult to integrate into projects that follow clean architecture or domain-driven design.

This library takes a different approach:

- **Domain-first, not NIP-first.** Code is organised around domain concepts (events, identities, tags, messages) rather than NIP numbers. A single `Event` entity handles creation, signing, and verification regardless of which NIP defines the event kind.
- **Clean Architecture with strict layer separation.** Domain entities and value objects have no framework dependencies. The only external library in the domain layer is cryptographic (secp256k1 elliptic curve math), which is intrinsic to Nostr identity. Bech32 encoding, JSON serialisation, and other infrastructure concerns live behind interfaces or in infrastructure adapters.
- **Immutable value objects and pure functions.** Events, tags, timestamps, and identities are all immutable. Factory methods are static. Services are stateless. No hidden side effects.
- **Designed for composition.** This is a core library, not an application. It provides the building blocks for relays, clients, and web applications without imposing architectural decisions on consumers.

Features
--------

[](#features)

- Complete Nostr protocol implementation
- Clean Architecture with strict layer separation
- Domain-driven design with pure business logic
- Comprehensive cryptographic support using secp256k1
- Native libsecp256k1 FFI acceleration covering BIP340 sign/verify, x-only pubkey derivation, NIP-44 ECDH, and group-law primitives (compressed scalar-base-mul, point-mul, point-add) — automatic pure-PHP fallback when the C library is unavailable
- Bech32 *and* bech32m encoding/decoding via a single `Bech32Codec`(NIP-19 prefixes plus BIP-350 variants used by FROSTR and other bech32m-prefixed consumers)
- Full NIP compliance validation
- Type-safe message handling with domain objects at all boundaries
- Optimised tag lookups via lazy indexing
- Extensive test coverage with PHPStan level 9

Requirements
------------

[](#requirements)

Declared in `composer.json`:

- PHP 8.3 or higher
- `ext-intl` (NFKC password normalisation in NIP-49)
- `ext-sodium` (NIP-44 and NIP-49 AEAD, `sodium_memzero`)

Used by the library but not declared as hard requirements, because several code paths are optional and the recommended typical usage will load them anyway:

- `ext-gmp` is needed by the pure-PHP signing and ECDH fallback (the documented path when `libsecp256k1` is unavailable). If you know you always have `libsecp256k1` installed and never invoke the pure-PHP path, this extension is not touched.
- `ext-mbstring` is needed by the search-filter matcher, `EventContent::getLength`, and the bech32 TLV decoder. Most consumers will hit one of these.
- `ext-ffi` is needed by NIP-49 (unconditionally) and by the `Secp256k1SignatureAdapter::create()` / `Secp256k1EcdhAdapter::create()` factories (for the `libsecp256k1` probe). Consumers who do not use NIP-49 and who construct the adapters directly with `new Secp256k1SignatureAdapter(null, ...)` / `new Secp256k1EcdhAdapter()` can run without `ext-ffi` at all and stay on the pure-PHP path.
- `libsodium` system library, reachable via FFI, is required for NIP-49 scrypt. Typically already installed wherever `ext-sodium` is installed.

### Optional (recommended)

[](#optional-recommended)

- `libsecp256k1` system library

When present, Schnorr signing, verification, public-key derivation, and NIP-44 ECDH use the native C library for significantly faster performance. Without it, the library falls back to a pure-PHP implementation via `paragonie/ecc` automatically.

Installation
------------

[](#installation)

```
composer require innis/nostr-core
```

Quick Start
-----------

[](#quick-start)

Cryptographic operations (signing, verification, public-key derivation, ECDH) are exposed as Domain service interfaces with Infrastructure adapters. The `Secp256k1SignatureAdapter` and `Secp256k1EcdhAdapter` pick an FFI-accelerated path when `libsecp256k1` is available and fall back to pure PHP otherwise — callers do not need to care.

### Key Generation

[](#key-generation)

```
use Innis\Nostr\Core\Domain\ValueObject\Identity\KeyPair;
use Innis\Nostr\Core\Infrastructure\Adapter\Secp256k1SignatureAdapter;

$signatureService = Secp256k1SignatureAdapter::create();
$keyPair = KeyPair::generate($signatureService);

echo $keyPair->getPrivateKey()->toBech32(); // nsec1...
echo $keyPair->getPublicKey()->toBech32();  // npub1...
```

### Event Creation and Signing

[](#event-creation-and-signing)

```
use Innis\Nostr\Core\Domain\Factory\EventFactory;

$event = EventFactory::createTextNote(
    $keyPair->getPublicKey(),
    'Hello Nostr!'
);

$signedEvent = $event->sign($keyPair, $signatureService);

$signedEvent->verify($signatureService); // bool
```

### NIP-44 Encryption

[](#nip-44-encryption)

Deriving a conversation key needs an ECDH service. `Secp256k1EcdhAdapter::create()` follows the same FFI-or-fallback pattern as the signature adapter:

```
use Innis\Nostr\Core\Domain\ValueObject\Identity\ConversationKey;
use Innis\Nostr\Core\Infrastructure\Adapter\Nip44EncryptionAdapter;
use Innis\Nostr\Core\Infrastructure\Adapter\Secp256k1EcdhAdapter;

$ecdhService = Secp256k1EcdhAdapter::create();
$conversationKey = ConversationKey::derive(
    $senderPrivateKey,
    $recipientPublicKey,
    $ecdhService,
);

$encryption = new Nip44EncryptionAdapter();
$ciphertext = $encryption->encrypt('Hello in private', $conversationKey);
$plaintext = $encryption->decrypt($ciphertext, $conversationKey);
```

Nonce generation is injected. `Nip44EncryptionAdapter` accepts an optional `RandomBytesGeneratorInterface` and defaults to `NativeRandomBytesGeneratorAdapter` (PHP's `random_bytes`) when none is supplied — that is the production path. Test suites inject a deterministic generator to reproduce the official NIP-44 vectors byte-for-byte. The adapter deliberately has no public `encryptWithNonce` method, because a caller-supplied nonce is a reuse footgun that catastrophically breaks ChaCha20 confidentiality; keeping nonce generation behind a port makes tests deterministic without giving production code a way to misuse it.

Always construct the adapters through their `::create()` factories. Direct instantiation via `new Secp256k1SignatureAdapter(null, ...)` or `new Secp256k1EcdhAdapter()` exists for dependency injection and testing but stays on the pure-PHP path regardless of whether `libsecp256k1` is installed.

### Message Handling

[](#message-handling)

```
use Innis\Nostr\Core\Infrastructure\Adapter\JsonMessageSerialiserAdapter;
use Innis\Nostr\Core\Domain\ValueObject\Protocol\Message\Client\EventMessage;

$serialiser = new JsonMessageSerialiserAdapter();

$eventMessage = new EventMessage($signedEvent);
$json = $eventMessage->toJson();

$deserialised = $serialiser->deserialiseClientMessage($json);
```

### Password-Encrypted Private Keys (NIP-49)

[](#password-encrypted-private-keys-nip-49)

The NIP-49 adapter takes the password as a `Closure(): string` rather than a raw string. The adapter invokes the closure exactly once, `sodium_memzero`s the revealed password before the method returns, and the caller never has to maintain a password binding in its own scope:

```
use Innis\Nostr\Core\Domain\Enum\KeySecurityByte;
use Innis\Nostr\Core\Domain\ValueObject\Identity\Ncryptsec;
use Innis\Nostr\Core\Domain\ValueObject\Identity\PrivateKey;
use Innis\Nostr\Core\Infrastructure\Adapter\Nip49EncryptionAdapter;

$adapter = new Nip49EncryptionAdapter();
$privateKey = PrivateKey::generate();

$ncryptsec = $adapter->encrypt(
    $privateKey,
    static fn (): string => readPasswordFromUser(),
    logN: 16,
    keySecurity: KeySecurityByte::ClientSideOnly,
);

$stored = (string) $ncryptsec; // ncryptsec1...

$decoded = Ncryptsec::fromString($stored);
$recovered = $adapter->decrypt($decoded, static fn (): string => readPasswordFromUser());
```

### Secret Key Lifecycle

[](#secret-key-lifecycle)

`PrivateKey` and `ConversationKey` hold their raw bytes inside a `SecretKeyMaterial` value object. Callers that need to clear secret material from memory can call `zero()`; any subsequent operation on that key throws `SecretKeyMaterialZeroedException`. Infrastructure code that genuinely needs raw bytes uses the bounded `expose` callback, which passes a CoW-separated copy of the bytes to the closure and `sodium_memzero`s that copy before the method returns:

```
$derived = $privateKey->expose(static function (string $bytes): string {
    return derive_something($bytes);
});

$privateKey->zero();
$signatureService->sign($privateKey, $message); // throws SecretKeyMaterialZeroedException
```

**`zero()` is a contract, not a guarantee via destruction.** `SecretKeyMaterial`'s destructor does call `zero()` as defence-in-depth, but PHP's garbage collector runs on refcount-zero, which may never happen for keys captured in long-lived closures, static state, exception trace frames, or cyclic references. Applications that require bounded key-material lifetimes — session-scoped bunker signers, for example — must call `$privateKey->zero()` explicitly at the end of the session. Treat the destructor as cleanup-of-last-resort, not as the primary wipe mechanism.

Supported NIPs
--------------

[](#supported-nips)

NIPDescriptionSupport[NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md)Basic protocol flowEvent creation, signing, verification, serialisation[NIP-02](https://github.com/nostr-protocol/nips/blob/master/02.md)Follow listKind 3 with contact list tags[NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md)Encrypted direct messagesKind 4 with recipient validation; `Nip04EncryptionAdapter` for AES-256-CBC encrypt/decrypt over a 32-byte ECDH shared secret[NIP-05](https://github.com/nostr-protocol/nips/blob/master/05.md)DNS-based identityIdentifier parsing and HTTP verification[NIP-09](https://github.com/nostr-protocol/nips/blob/master/09.md)Event deletionKind 5 with deletion tag validation and `isDeletion()` detection[NIP-10](https://github.com/nostr-protocol/nips/blob/master/10.md)Reply conventionsReply chain analysis with root/reply/mention markers[NIP-11](https://github.com/nostr-protocol/nips/blob/master/11.md)Relay informationRelay metadata fetching and parsing[NIP-17](https://github.com/nostr-protocol/nips/blob/master/17.md)Private direct messagesKind 14 with NIP-44 encryption and gift wrap (kind 1059/1060)[NIP-18](https://github.com/nostr-protocol/nips/blob/master/18.md)RepostsKind 6/16 with embedded event extraction and quote detection[NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md)Bech32 encodingnpub, nsec, note, nprofile, nevent, naddr encoding/decoding; `Bech32Codec` also supports the BIP-350 bech32m variant for non-NIP consumers (e.g. FROSTR `bfgroup1…` / `bfshare1…` / `bfonboard1…`) via the `Bech32Variant` enum[NIP-22](https://github.com/nostr-protocol/nips/blob/master/22.md)CommentsKind 1111 with root/parent kind tags and reply chain analysis[NIP-23](https://github.com/nostr-protocol/nips/blob/master/23.md)Long-form contentKind 30023 as parameterised replaceable events[NIP-25](https://github.com/nostr-protocol/nips/blob/master/25.md)ReactionsKind 7 event support[NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md)Public chatKind 40-44 channel event types[NIP-40](https://github.com/nostr-protocol/nips/blob/master/40.md)ExpirationEvent expiration detection via `isExpired()`[NIP-42](https://github.com/nostr-protocol/nips/blob/master/42.md)AuthenticationAUTH message handling and challenge detection[NIP-44](https://github.com/nostr-protocol/nips/blob/master/44.md)Encrypted payloadsNIP-44 v2 encrypt/decrypt with ECDH, ChaCha20, HMAC-SHA256[NIP-45](https://github.com/nostr-protocol/nips/blob/master/45.md)CountingCOUNT relay message support[NIP-49](https://github.com/nostr-protocol/nips/blob/master/49.md)Private key encryptionPassword-encrypted `ncryptsec` with scrypt + XChaCha20-Poly1305[NIP-50](https://github.com/nostr-protocol/nips/blob/master/50.md)SearchSearch filter support[NIP-51](https://github.com/nostr-protocol/nips/blob/master/51.md)ListsAll standard list kinds (10000-10102) and set kinds (30000-39092)[NIP-57](https://github.com/nostr-protocol/nips/blob/master/57.md)Lightning zapsZap request/receipt parsing, BOLT-11 amount extraction[NIP-61](https://github.com/nostr-protocol/nips/blob/master/61.md)NutzapsKind 9321 cashu proof parsing and amount extraction[NIP-70](https://github.com/nostr-protocol/nips/blob/master/70.md)Protected eventsProtected event detection via `isProtected()`[NIP-98](https://github.com/nostr-protocol/nips/blob/master/98.md)HTTP authKind 27235 validation: signature, URL, method, payload hash, timestamp tolerancePerformance
-----------

[](#performance)

### Native FFI Acceleration

[](#native-ffi-acceleration)

The library can use the system's native `libsecp256k1` C library via PHP's FFI extension for cryptographic operations. This provides significant performance gains for applications performing bulk signature verification (relays, indexers) or threshold-signature math (FROSTR signers).

Operations routed through `LibSecp256k1Ffi` when the library is loaded:

- `sign` — BIP340 Schnorr sign
- `verify` — BIP340 Schnorr verify
- `derivePublicKey` — secret to 32-byte x-only pubkey
- `derivePublicKeyCompressed` — secret to 33-byte compressed pubkey (parity-aware)
- `computeSharedX` — x-only ECDH for NIP-44 conversation keys
- `pointMulCompressed` — arbitrary-base scalar multiplication on a compressed point
- `pointAddCompressed` — group addition of two compressed points

The last three primitives are what threshold-signature consumers like [`innis/frostr-core`](https://github.com/innis-xyz/frostr-core) need for FROST partial signing, partial ECDH and dealer setup. With FFI loaded, those operations run roughly 60× faster than the pure-PHP fallback.

To install the native library:

```
# Ubuntu/Debian
sudo apt install libsecp256k1-1

# macOS (Homebrew)
brew install libsecp256k1
```

No code changes are required. The library detects and uses the native implementation automatically, falling back to pure PHP when unavailable.

Architecture
------------

[](#architecture)

This package follows Clean Architecture principles with strict layer separation:

- **Domain Layer**: Pure business logic, immutable entities and value objects (cryptographic library is the sole external dependency, used directly by identity value objects)
- **Application Layer**: Port interfaces for external service integration
- **Infrastructure Layer**: External adapters and implementations

Dependencies
------------

[](#dependencies)

PackagePurpose`paragonie/ecc`Pure-PHP secp256k1 elliptic curve operations (fallback when FFI unavailable)`paragonie/sodium_compat`ChaCha20 primitives used by NIP-44`psr/log`PSR-3 logger interface for infrastructure servicesTesting
-------

[](#testing)

```
# Full suite: Unit + Integration + Compliance + PHPStan (ship gate)
composer test

# Unit suite only (fast inner loop; skips compliance property fuzz)
composer test-unit

# PHPStan analysis (level 9)
composer analyse

# Fix code style
composer fix-style
```

Filter-set hash
---------------

[](#filter-set-hash)

`FilterHasher::hash` (PHP `@innis/nostr-core`) and `hashFilters` (TypeScript `@innis/nostr-core`) compute a stable identity for a NIP-01 `REQ` filter set, suitable as a subscription dedup key. Both follow the same canonicalisation spec:

1. Represent the filter set as an ordered list of filters in wire form (PHP: `Filter::toArray()`; TS: `NostrFilter` objects).
2. Canonicalise recursively:
    - **object / map** — sort keys ascending (bytewise), then canonicalise each value;
    - **array / list** — canonicalise each element, then sort the elements ascending (bytewise) by their canonical encoding;
    - **scalar** — left unchanged.
3. Encode the canonicalised structure as **ASCII-safe JSON**: compact (no inserted whitespace), `/` left unescaped (`JSON_UNESCAPED_SLASHES`), and every non-ASCII code unit escaped as a lowercase `\uXXXX` (astral characters as UTF-16 surrogate pairs) — i.e. `json_encode` *without* `JSON_UNESCAPED_UNICODE`. The TS side post-escapes `JSON.stringify` to match.
4. The hash is the lowercase-hex **SHA-256** of that canonical string.

Because object keys, array elements, and the filters themselves are all sorted, two filter sets that select the same events produce the same digest regardless of how they were ordered on input.

### Cross-language parity

[](#cross-language-parity)

The two implementations are **byte-for-byte identical for every input**, including non-ASCII `search` strings and tag-filter values. Making the canonical form ASCII-safe (step 3) is the mechanism: with no raw non-ASCII bytes, bytewise / UTF-8-byte / UTF-16-code-unit / code-point collation all coincide, so both runtimes sort and encode identically — closing both the `json_encode`-vs-`JSON.stringify` escaping gap (`U+2028` / `U+2029`) and the UTF-8-vs-UTF-16 sort gap (astral characters such as emoji).

Parity is locked by shared conformance anchors asserted in both test suites — equivalent inputs must hash to the same digest in both:

InputSHA-256 digest`[]` (empty set)`4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945``[{}]` (one empty filter)`e10808d43975dc400731053386849f864f297e6c4f7519c380f3dbaf7067a840``[{ "kinds": [2,1], "limit": 5 }]``a34519033f2032b87a019ef94f4be40fc1ab6a621d2b66c55b0d386c3e576587``[{ "search": "U+2028" }]``aee96085e5802e7b70a145ffdf6aa7e2335469aa223be66c79c9ad1699ecd7f2``[{ "search": "U+1F600" }]` (astral)`ac283a84cb87cd19a956f552a82cb9155fc1a980d576356c4d987e71710a4dd3``[{ "#t": ["U+1F600","U+1F4A9"] }]` (astral sort)`a47382ebe89a655c3d9d1e27a1e5e445ca0dd4f5348e72f518b2a98b6f77f92b`License
-------

[](#license)

MIT License. See LICENSE file for details.

###  Health Score

44

—

FairBetter than 91% of packages

Maintenance95

Actively maintained with recent releases

Popularity13

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Total

28

Last Release

25d ago

PHP version history (2 changes)v0.1.0PHP ^8.1

v0.1.1PHP ^8.3

### Community

Maintainers

![](https://www.gravatar.com/avatar/d2ca81e761ec3b2d4ce98ac59cd4394bb4d70ebf3d1e620ff154ffcfc7a34bfc?d=identicon)[innis](/maintainers/innis)

---

Top Contributors

[![johninnis](https://avatars.githubusercontent.com/u/242370111?v=4)](https://github.com/johninnis "johninnis (81 commits)")

---

Tags

protocolcoredomainnostr

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/innis-nostr-core/health.svg)

```
[![Health](https://phpackages.com/badges/innis-nostr-core/health.svg)](https://phpackages.com/packages/innis-nostr-core)
```

###  Alternatives

[symfony/lock

Creates and manages locks, a mechanism to provide exclusive access to a shared resource

514135.1M625](/packages/symfony-lock)[matomo/matomo

Matomo is the leading Free/Libre open analytics platform

21.6k38.2k](/packages/matomo-matomo)[phpro/soap-client

A general purpose SoapClient library

8895.9M52](/packages/phpro-soap-client)[ecotone/ecotone

Enterprise architecture layer for Laravel and Symfony — CQRS, Event Sourcing, Durable Workflows (Sagas, Orchestrators), Projections, and Outbox messaging via PHP attributes.

562565.8k42](/packages/ecotone-ecotone)[civicrm/civicrm-core

Open source constituent relationship management for non-profits, NGOs and advocacy organizations.

749284.3k35](/packages/civicrm-civicrm-core)[illuminate/broadcasting

The Illuminate Broadcasting package.

7126.9M203](/packages/illuminate-broadcasting)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
