NSpark.Signer.Wire
0.1.0-alpha.1
dotnet add package NSpark.Signer.Wire --version 0.1.0-alpha.1
NuGet\Install-Package NSpark.Signer.Wire -Version 0.1.0-alpha.1
<PackageReference Include="NSpark.Signer.Wire" Version="0.1.0-alpha.1" />
<PackageVersion Include="NSpark.Signer.Wire" Version="0.1.0-alpha.1" />
<PackageReference Include="NSpark.Signer.Wire" />
paket add NSpark.Signer.Wire --version 0.1.0-alpha.1
#r "nuget: NSpark.Signer.Wire, 0.1.0-alpha.1"
#:package NSpark.Signer.Wire@0.1.0-alpha.1
#addin nuget:?package=NSpark.Signer.Wire&version=0.1.0-alpha.1&prerelease
#tool nuget:?package=NSpark.Signer.Wire&version=0.1.0-alpha.1&prerelease
nspark-signer
Multi-language remote signer for the Spark protocol. Holds wallet keys outside the process that orchestrates Spark flows — your gRPC server (e.g. nspark) calls into the signer over a long-lived bidirectional gRPC stream to get signatures, ECIES decrypts, FROST partials, and tweak-batch payloads.
dotnet add package NSpark.Signer.Wire
The canonical wire protocol is spark.signer.v1alpha1, defined under
proto/. Per-language implementations live in language-named subdirectories.
Why this exists
Real wallets hold private keys on user-controlled devices (phones, laptops, hardware wallets) — not on the server that builds transactions. The signer needs to initiate the connection (devices can't accept inbound from behind NAT), but the server is the one that knows when a signature is needed. So the gRPC topology is inverted: the signer is the client; the server pushes signing requests down an open bidi stream.
Signer (Swift / Kotlin / …) Server (your gRPC service, hosts nspark)
│ │
│ ──── opens bidi RPC + auth header ──────────►│
│ ◄─── Challenge (32 random bytes) ────────────│
│ ──── Hello (identity_proof) ────────────────►│ verifies proof + ownership
│ ◄─── Welcome ────────────────────────────────│
│ │
│ ◄─── Request { sign_with_identity_key, … } ──│ ← server-pushed signing call
│ ──── Response { der_signature: … } ─────────►│
│ │
│ (… more Requests / Responses …) │
The Hello.identity_proof is an ECDSA signature over the server's Challenge.nonce,
made with the wallet's identity private key. It proves the signer holds the key —
defends against stolen-token replay where someone might try to claim a wallet they
don't control.
Quick start
You have a .NET gRPC server. You want to host the signer endpoint and call into it when your code needs to sign:
// Program.cs
using NSpark.Signer.Wire;
builder.Services.AddRemoteSparkSigner();
builder.Services.AddScoped<ISignerAuthenticator, YourTokenAuth>();
builder.Services.AddScoped<IWalletOwnershipResolver, YourWalletDb>();
var app = builder.Build();
app.MapRemoteSparkSigner(); // exposes RemoteSigner.Connect on the existing Kestrel
app.Run();
// In any endpoint where you need to sign:
public async Task<IResult> Send(
SendRequest req,
IRemoteSparkSignerProvider signers,
SparkConnection spark,
CancellationToken ct)
{
var signer = signers.GetSigner(req.WalletPubkey); // throws SignerUnavailableException if offline
var wallet = await spark.CreateWalletAsync(signer, ct);
var transfer = await wallet.SendAsync(req.ReceiverPubkey, req.AmountSats, ct: ct);
return Results.Ok(transfer);
}
Two interfaces to implement. Four lines in Program.cs. The library handles the
handshake, identity-proof verification, per-wallet routing, the 17-method
ISparkSigner adapter, heartbeats, cancellation, and connection lifecycle.
Full integration guide: docs/integrators.md.
Repo layout
proto/ canonical wire protocol (spark.signer.v1alpha1)
spark/signer/v1alpha1/
signer_service.proto service + envelopes
signer_session.proto handshake, error codes, capabilities
signer_requests.proto 17 *Request/*Response method pairs
signer_types.proto SigningCommitment, SoTarget, NonceHandle, …
spark_orchestration.proto types embedded as ECIES payloads (mirrors nspark spark.proto)
dotnet/
src/
NSpark.Signer.Wire/ .NET server library — drop into your gRPC host
test-server/ runnable test server + HTTP fixture endpoints
swift/
Sources/
NSparkSigner/ Swift Package — embed in iOS/macOS host apps
NSparkSignerTestSupport/ spawn-test-server + HTTP clients for fixtures
Tests/
NSparkSignerTests/ unit tests
NSparkSignerIntegrationTests/ live-network integration tests (gated on env vars)
docs/
protocol.md wire protocol spec
integrators.md .NET host integration guide
swift-client.md Swift host embed guide
trust-model.md security boundaries + threat model
CHANGELOG.md version history
Status
Verified live on Spark mainnet (23 integration tests, all green):
- All 17
NSpark.Signer.ISparkSignermethods proxy correctly over the wire signer - Real Spark-to-Spark transfers, Lightning send + receive (claim), token transfer + mint + burn
- Identity-proof handshake with NBitcoin verification
- Opaque UUID nonce/adaptor handles with TTL eviction
- Swift client reconnect loop with exponential backoff
- Cancellation propagation (server
Cancel→ in-flight Swift task cancelled) - Pluggable
ISignerAuthenticator+IWalletOwnershipResolver
Not yet wired (intentionally — see trust model):
- TLS guidance and helpers (you bring your own Kestrel TLS config for now)
- Rate limiting / per-signer request quotas
- Metrics + structured tracing
- Multi-instance signer registry (current registry is in-process; multi-instance topologies need sticky routing or a shared registry — flagged in docs)
- NuGet / SwiftPM published artifacts (still consumed via project / path refs)
Languages
| Language | Role | State | Path |
|---|---|---|---|
| Swift | Signer (client) | Working | swift/Sources/NSparkSigner/ |
| .NET | Server (host) | Working | dotnet/src/NSpark.Signer.Wire/ |
| Kotlin | Signer (client) | Planned | kotlin/ (future) |
The .proto files are language-neutral — adding a new signer implementation means
generating client stubs from proto/spark/signer/v1alpha1/ and implementing the 17
method handlers + the bidi stream dispatcher. Reference: see how Swift does it under
swift/Sources/NSparkSigner/Dispatcher/RequestDispatcher.swift and
Transport/BidiStreamConnection.swift.
Versioning
Wire protocol package is spark.signer.v1alpha1. Breaking changes are allowed
while we're in v1alpha1 — we'll promote to v1 once at least two non-Swift
implementations have integrated. See CHANGELOG.md.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- Google.Protobuf (>= 3.29.3)
- Grpc.AspNetCore (>= 2.66.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Options (>= 9.0.0)
- NBitcoin (>= 8.0.10)
- NSpark (>= 0.2.0-alpha.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0-alpha.1 | 86 | 5/27/2026 |