Privacy, control, and safety
Retained old keys would let Lightspark and Flashnet sign a conflicting spend of a Lightspark-funded Lightning leaf
In Spark's reference inbound-Lightning flow, the SSP is the former owner of the leaf. Under the published two-of-three configuration, retained matching keys from the Lightspark SSP, Lightspark Operator, and Flashnet Operator would be sufficient to sign a fresh conflicting spend; no evidence shows retention or collusion. 123456789
What the evidence shows
Technical and product details
- Documented behavior
Spark's reference inbound-Lightning flow has the SSP create and own a leaf, transfer it to the receiving user, and deliver the next user-side signing key; the receiver then claims the transfer with a newly generated final key. 3
Supporting material shown on this page
reference SSP-funded Lightning receivePinned source code · github.com · checked 2026-07-14 Relevant lines from the pinned source · open raw file 119 // Create user and ssp configs 120 userConfig := wallet.NewTestWalletConfig(t) 121 sspConfig := wallet.NewTestWalletConfig(t) 122 // User creates an invoice 123 amountSats := uint64(100) 124 preimage, paymentHash := testPreimageHash(t, amountSats) 125 fakeInvoiceCreator := NewFakeLightningInvoiceCreator() - Documented behavior
The published mainnet default points the SSP to Lightspark and uses a two-of-three Operator set comprising Lightspark, Breez, and Flashnet. 12
Supporting material shown on this page
default mainnet SSPPinned source code · github.com · checked 2026-07-14 Relevant lines from the pinned source · open raw file 11const SSP_IDENTITY_PUBLIC_KEYS = { 12 LOCAL: "028c094a432d46a0ac95349d792c2e3730bd60c29188db716f56a99e39b95338b4", 13 REGTEST: { 14 PROD: "022bf283544b16c0622daecb79422007d167eca6ce9f0c98c0c49833b1f7170bfe", 15 }, 16 MAINNET: { 17 PROD: "023e33e2920326f64ea31058d44777442d97d7d5cbfcf54e3060bc1695e5261c93",published mainnet Operator configurationPinned source code · github.com · checked 2026-07-14 Relevant lines from the pinned source · open raw file 324 baseUrl: getSspUrl("REGTEST"), 325 identityPublicKey: getSspIdentityPublicKey("REGTEST"), 326 }, 327}; 328 329const MAINNET_WALLET_CONFIG: Required<ConfigOptions> = { 330 ...BASE_CONFIG, - Documented behavior
Spark's transfer construction adds the difference between the old and new user keys to the Operator key and then derives the new owner key from the unchanged leaf verifying key. Algebraically,
new_user + new_operator = old_user + old_operator = leaf_verifying_key. A matching historical user key and threshold set of historical Operator shares therefore remain a valid signing state if all were retained. 4567Supporting material shown on this page
client transfer-key tweakPinned source code · github.com · checked 2026-07-14 Relevant lines from the pinned source · open raw file 417 const sparkFrost = getSparkFrost(); 418 return { 419 shares: await this.splitSecretWithProofs({ 420 secret: resultPrivKey, 421 curveOrder, 422 threshold, 423 numShares,server share update and invariant verifying keyPinned source code · github.com · checked 2026-07-14 Relevant lines from the pinned source · open raw file 116 if err != nil || keyshare == nil { 117 return nil, fmt.Errorf("unable to tweak keyshare %s for leaf %s: %w", keyshareID, req.GetLeafId(), err) 118 } 119 120 signingPubkey := leaf.VerifyingPubkey.Sub(keyshare.PublicKey) 121 return leaf.Update().SetOwnerSigningPubkey(signingPubkey), nil 122}one-plus-threshold signing constructionPinned source code · github.com · checked 2026-07-14 Relevant lines from the pinned source · open raw file 453type KeyPackageProvider func(ctx context.Context, config *so.Config, keyshareIDs []uuid.UUID) (map[uuid.UUID]*pbfrost.KeyPackage, error) 454 455func SignFrostInternal(ctx context.Context, config *so.Config, jobs []*SigningJob, getKeyPackages KeyPackageProvider, sparkServiceClientFactory SparkServiceFrostSignerFactory) ([]*SigningResult, error) { 456 selection := OperatorSelection{Option: OperatorSelectionOptionThreshold, Threshold: int(config.Threshold)} 457 signingKeyshareIDs := SigningKeyshareIDsFromSigningJobs(jobs) 458 signingKeyshares, err := getKeyPackages(ctx, config, signingKeyshareIDs) 459 if err != nil {Spark key-deletion trust assumptionOfficial documentation · docs.spark.money · checked 2026-07-14 - Documented behavior
For a Lightspark-SSP-funded leaf, the former-owner role and one Operator role are at Lightspark; the Flashnet Operator supplies the second share needed by the published threshold. Flashnet separately says it helped build Spark, and a Lightspark co-founder's published CV lists an advisory role at Flashnet. Those documented working and personnel ties are context only—not evidence of common control, shared key access, or collusion. 12689
Supporting material shown on this page
default mainnet SSPPinned source code · github.com · checked 2026-07-14 Relevant lines from the pinned source · open raw file 11const SSP_IDENTITY_PUBLIC_KEYS = { 12 LOCAL: "028c094a432d46a0ac95349d792c2e3730bd60c29188db716f56a99e39b95338b4", 13 REGTEST: { 14 PROD: "022bf283544b16c0622daecb79422007d167eca6ce9f0c98c0c49833b1f7170bfe", 15 }, 16 MAINNET: { 17 PROD: "023e33e2920326f64ea31058d44777442d97d7d5cbfcf54e3060bc1695e5261c93",published mainnet Operator configurationPinned source code · github.com · checked 2026-07-14 Relevant lines from the pinned source · open raw file 324 baseUrl: getSspUrl("REGTEST"), 325 identityPublicKey: getSspIdentityPublicKey("REGTEST"), 326 }, 327}; 328 329const MAINNET_WALLET_CONFIG: Required<ConfigOptions> = { 330 ...BASE_CONFIG,one-plus-threshold signing constructionPinned source code · github.com · checked 2026-07-14 Relevant lines from the pinned source · open raw file 453type KeyPackageProvider func(ctx context.Context, config *so.Config, keyshareIDs []uuid.UUID) (map[uuid.UUID]*pbfrost.KeyPackage, error) 454 455func SignFrostInternal(ctx context.Context, config *so.Config, jobs []*SigningJob, getKeyPackages KeyPackageProvider, sparkServiceClientFactory SparkServiceFrostSignerFactory) ([]*SigningResult, error) { 456 selection := OperatorSelection{Option: OperatorSelectionOptionThreshold, Threshold: int(config.Threshold)} 457 signingKeyshareIDs := SigningKeyshareIDsFromSigningJobs(jobs) 458 signingKeyshares, err := getKeyPackages(ctx, config, signingKeyshareIDs) 459 if err != nil {Flashnet build relationshipBlog or announcement · flashnet.xyz · checked 2026-07-14 published Lightspark-Flashnet advisory roleWeb source · static1.squarespace.com · checked 2026-07-14
Interpretation
Why this matters for Spark's trust model
If Lightspark retained the SSP's matching former owner key and old Lightspark Operator share, and Flashnet retained its matching old Operator share, those roles possess enough cryptographic material to sign a fresh transaction conflicting with the recipient's still-unspent leaf. The nonparticipation of Breez would not block that signature. 1234567
This section states the trust-model consequence of the cited evidence. Conditional or inferred claims remain bounded by the next section.
Scope control
What this does not establish
- This is a conditional cryptographic result, not an allegation of conduct. The keys must match the same leaf and historical state. Public evidence does not show retention, collusion, a conflicting signature, or a theft; the mainnet configuration is published client code rather than a live server attestation, and the production Lightspark SSP is not public. The attack assumes modified or offline signing behavior, not honest API use, and directly concerns the same unspent leaf rather than unrelated wallet funds. 123789
Sources and excerpts
Primary sources
The relevant details and available source-code excerpts appear alongside the claims above. This list preserves the complete original-source trail for independent verification.
default mainnet SSP
Pinned source code · github.com · Pinned or archival · checked 2026-07-14
published mainnet Operator configuration
Pinned source code · github.com · Pinned or archival · checked 2026-07-14
reference SSP-funded Lightning receive
Pinned source code · github.com · Pinned or archival · checked 2026-07-14
client transfer-key tweak
Pinned source code · github.com · Pinned or archival · checked 2026-07-14
server share update and invariant verifying key
Pinned source code · github.com · Pinned or archival · checked 2026-07-14
one-plus-threshold signing construction
Pinned source code · github.com · Pinned or archival · checked 2026-07-14
Spark key-deletion trust assumption
Official documentation · docs.spark.money · Live source · checked 2026-07-14
Flashnet build relationship
Blog or announcement · flashnet.xyz · Live source · checked 2026-07-14
published Lightspark-Flashnet advisory role
Web source · static1.squarespace.com · Live source · checked 2026-07-14
How sources, absence findings, and limitations were evaluated