Review of Spark's trust model

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

9 sourcesEvidence reviewed through July 14, 2026

What the evidence shows

Technical and product details

  1. 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()
  2. 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,
  3. 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. 4567

    Supporting 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 {
  4. 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 {

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.

1

default mainnet SSP

Pinned source code · github.com · Pinned or archival · checked 2026-07-14

Open original source
2

published mainnet Operator configuration

Pinned source code · github.com · Pinned or archival · checked 2026-07-14

Open original source
3

reference SSP-funded Lightning receive

Pinned source code · github.com · Pinned or archival · checked 2026-07-14

Open original source
4

client transfer-key tweak

Pinned source code · github.com · Pinned or archival · checked 2026-07-14

Open original source
5

server share update and invariant verifying key

Pinned source code · github.com · Pinned or archival · checked 2026-07-14

Open original source
6

one-plus-threshold signing construction

Pinned source code · github.com · Pinned or archival · checked 2026-07-14

Open original source
7

Spark key-deletion trust assumption

Official documentation · docs.spark.money · Live source · checked 2026-07-14

Open original source
8

Flashnet build relationship

Blog or announcement · flashnet.xyz · Live source · checked 2026-07-14

Open original source
9

published Lightspark-Flashnet advisory role

Web source · static1.squarespace.com · Live source · checked 2026-07-14

Open original source

How sources, absence findings, and limitations were evaluated