spark.exposed

ReviewedOperator threat model

What malicious operators can do

One operator can veto several ordinary wallet flows and expose wallet metadata. Two control the operator signing threshold. Three remove the last honest operator source of help. The user's Bitcoin signing contribution remains a separate requirement.

A theoretical assessment of specific code paths. Production settings and actual operator misconduct are not established by this review. [S01] [S07] [S04]

Read the columns this way

The model and its limits

This model uses the three-operator, two-share configuration in the reviewed Breez Spark SDK. The columns count malicious signing operators (SOs); all remaining operators are assumed honest. [S01] [S07]

Static source tracing and theoretical capability analysis at the listed revisions. No production attack was attempted. The review does not verify deployed binaries, runtime allowlists, current KYC policies, historical-key retention, or a successful theft.

Action-by-action comparison

What changes at 1, 2 and 3?

Yes
The stated operator control is sufficient within the row's scope and conditions.
No
That control alone is insufficient for this action. Read the conditions; this is not a general fund-safety guarantee.
Conditional
Additional keys, state, identifying information or an optional code path are required.
Its / their / all servers
Denial is limited to the endpoints the attacker controls.

Showing all 37 actions

Capabilities of 1, 2 or 3 malicious operators in the reviewed three-operator configuration. Remaining operators are honest. Every result includes the conditions in its row.
Action1 operator2 operators3 operatorsConditions and evidence
Wallet operations
Block a Spark send YesYesYes

StartTransferV3; also V2 with a transfer package. Refuse required preparation.

[S08] [S09] [S04]Code and threshold inference
Block claiming an incoming Spark transfer YesYesYes

ClaimTransfer consensus path. A transfer notification is not a completed claim.

[S10] [S04]Code and threshold inference
Block Bitcoin deposit finalization into Spark YesYesYes

FinalizeDepositTreeCreation. This does not prevent the Bitcoin deposit transaction itself.

[S11] [S04]Code and threshold inference
Block normal Lightning receive-invoice setup YesYesYes

StorePreimageShareV2 checks the wallet block on every SO during preparation.

[S12] [S13]Code and threshold inference
Block outgoing Lightning payment initiation YesYesYes

InitiatePreimageSwapV3 send path; blocks the Spark funding/swap step.

[S14] [S04]Code and threshold inference
Block incoming Lightning swap settlement into Spark YesYesYes

InitiatePreimageSwapV3 receive path. Does not by itself establish that a payer loses money.

[S14] [S28]Code and threshold inference
Block completion of a pending HODL swap YesYesYes

ProvidePreimage in the pending states that require consensus; some retries skip it.

[S15] [S16]Code and threshold inference
Block a cooperative Bitcoin withdrawal YesYesYes

CooperativeExitV2 with a transfer package; unilateral exit is a separate path.

[S17] [S18]Code and threshold inference
Block leaf renewal / timelock refresh YesYesYes

RenewLeaf requires every participant.

[S19] [S04]Code and threshold inference
Block completion of the reviewed leaf-optimization swap YesYesYes

The reviewed SDK must claim the incoming counter-transfer through all-operator ClaimTransfer preparation, so one refusing participant can prevent completion. The primary-swap consensus flag remains an additional conditional gate (default off). Other API versions or already-committed claims can differ.

[S45] [S10] [S04] [S20] [S21] [S07]SDK counter-transfer claim traced to all-participant preparation
Block a new static-deposit UTXO refund YesYesYes

InitiateStaticDepositUtxoRefund consensus path; completed-refund re-signing can differ.

[S22] [S23]Code and threshold inference
Block cooperative recovery of a watchtower-exited leaf YesYesYes

First RecoverWatchtowerExitedLeaf; already-recovered fee-bump signing skips this state transition.

[S24]Code and threshold inference
Policy and availability
Condition the affected wallet operations on KYC YesYesYes

An operator can refuse required cooperation until an identity check is completed. This is a policy capability inferred from refusal, not evidence of current mandatory KYC.

[S25] [S13] [S04]Code and threshold inference
Stop new threshold signing by withholding only their own shares NoYesYes

With one refusing SO, two honest shares remain sufficient cryptographically; all-participant preparation is a separate obstacle.

[S01] [S07]Code and threshold inference
Deny wallet read requests on controlled operator servers Its serverTheir serversAll servers

Each controls only its own operator endpoints. An honest survivor can still offer reads; usable wallet failover is a separate implementation question.

[S33]Threat-model inference
SSP access
Exclude a competing SSP from normal Lightning receive swaps YesYesYes

Official sender allowlist is checked on every participant. Defaults off; production setting unverified.

[S27] [S28] [S14]Code and threshold inference
Deny an SSP access to private operator RPCs Its serverTheir serversAll servers

Private-operation allowlisting was inspected in the open-ssp operator fork with authentication enabled. This does not establish the official mainnet RPC surface or its permissions.

[S31]Threat-model inference
Prevent someone from writing or hosting independent SSP software NoNoNo

They can deny service on their Spark deployment, not prohibit the software or a different operator set.

[S30] [S29]Threat-model inference
Take over an independent SSP Lightning node solely through SO control NoNoNo

Requires additional compromise or control of that separate service.

[S30]Threat-model inference
Stop unrelated Lightning payments outside this Spark deployment NoNoNo

SO authority does not confer control over unrelated Lightning nodes.

[S30]Threat-model inference
Privacy
Read available wallet balances and transfer relationships in operator records YesYesYes

Applies to records available at the controlled SO; not a guarantee every historical record was retained.

[S05] [S06]Code and threshold inference
Publish operator-held wallet metadata despite private mode YesYesYes

Private mode restricts server responses; it does not hide the database from its operator.

[S32] [S05]Code and threshold inference
Log the IP address connecting to the operator YesYesYes

Inference from control of a server receiving connections, not evidence that IP logs are currently retained. The visible peer may be a proxy or Tor exit, not the user's device.

[S01]Threat-model inference
Associate a wallet with a real person ConditionalConditionalConditional

Requires identifying information, such as a cooperating counterparty or service record. A public key alone is not a legal name.

[S13]Threat-model inference
Extract the public identity from an invoice embedding a Spark address YesYesYes

Requires zero malicious SOs: any invoice holder can decode it. Not all invoices embed it.

[S35] [S34]Code and threshold inference
Keys and funds
Reconstruct the operator Bitcoin signing-key component from their shares NoYesYes

Requires matching usable shares. The separate user component is still missing.

[S01] [S07]Code and threshold inference
Reconstruct a pending receive preimage from their own stored shares NoYesYes

Two matching stored shares of an ordinary non-HODL receive can reveal the preimage before authorized release. Excludes HODL swaps without such shares and data legitimately released by others during a swap. Collecting a Lightning payment also requires a party able to use the secret.

[S36] [S37] [S38] [S39]Code and threshold inference
Create an arbitrary Bitcoin spend using only current operator shares NoNoNo

A valid corresponding user contribution is still required; specific pre-signed transactions are a different case.

[S07]Code and threshold inference
Create a fresh conflicting Bitcoin spend by colluding with a former owner NoConditionalConditional

Requires two matching retained historical operator shares, the matching former-owner key, and a spendable output with the necessary ancestry. The combined Bitcoin key survives the ownership change. One matching share alone is insufficient.

[S07] [S40] [S41]Code and threshold inference
Reassign on-chain Bitcoin ownership merely by editing an operator database NoNoNo

Changing a database record does not change Bitcoin spending conditions.

[S07] [S40]Threat-model inference
Recovery
Withhold their own operator-held exit records YesYesYes

Honest surviving operators or independent backups may still provide the missing data.

[S42] [S43]Threat-model inference
Withhold every operator-held copy of exit records NoNoYes

One or two cannot control copies held by honest operators. Actual record completeness and retrieval support still matter.

[S44] [S42]Threat-model inference
Erase an independently stored user recovery backup through SO control alone NoNoNo

Would require additional control of the user device or backup service.

[S42]Threat-model inference
Withdraw all operator-provided watchtower assistance NoNoYes

Honest operators can continue their assistance; independent watchtowers remain separate.

[S44]Threat-model inference
Invalidate a complete, already-signed Bitcoin exit merely by freezing the wallet NoNoNo

Does not remove chain, fee, timelock, monitoring, or conflicting-spend risks.

[S42] [S43] [S44]Threat-model inference
Destroy the signing threshold by permanently deleting their current shares NoYesYes

For affected keys, permanently losing two matching current secret shares leaves only one. Assumes no recoverable matching copies. Wiping metadata alone is different; existing valid Bitcoin recovery paths remain separate.

[S01] [S07] [S41]Code and threshold inference

The invoice-decoding row needs zero malicious operators when the invoice embeds the address. “No” in the spend rows is limited to the stated key material, not every theft or stale-exit scenario.

One operator can censor without a signing majority

Suppose operator B wants to freeze Alice's ordinary Spark activity while serving everyone else. In the listed all-participant flows, B sees the affected identity and can refuse preparation. Every selected participant must succeed, so two willing signers do not by themselves get that operation through. [S02] [S03] [S04] [S06]

The built-in wallet refusal control has different enforcement points. A transfer claim checks it at the coordinator; merely changing a non-coordinator's switch does not establish a universal claim freeze. Storing receive-preimage shares checks the wallet at every participant. A malicious participant can also refuse beyond the places where the built-in control is wired. [S26] [S13] [S25]

Changing the coordinator does not remove a refusing operator from a flow that still requires every participant. Making identification a condition of cooperation is therefore a credible censorship scenario. Restoring service by reconfiguring operators is a separate intervention, not an established ordinary-wallet bypass. [S01] [S02] [S04]

An independent SSP still depends on operator cooperation

An SSP supplies services such as Lightning swaps and liquidity; it need not be run by a signing operator. The reviewed open-ssp implementation has its own wallet and Lightning backend, but calls the operator receive-swap API to deliver Spark funds. [S30] [S29]

The official operator code includes an optional allowlist of senders allowed to initiate Lightning receive swaps. Every participant checks it. One operator could enable the gate, exclude a competing SSP's sender identity and continue admitting another provider. The gate defaults off; its production setting and any actual exclusion were not verified. [S27] [S28] [S14]

That power can obstruct the SSP's normal receive service on this operator set. It does not prohibit someone from hosting the software, running their own Lightning node or using another operator deployment. Separate private RPC permissions found in the open-ssp fork are identified as fork evidence in the grid. [S30] [S31]

An invoice can lead to a wallet identity, then a refusal

If a BOLT11 invoice embeds a Spark address, anyone holding the invoice can extract that identity: zero malicious operators are needed. That is the scope of spark-invoice-doxxer. Not every invoice includes the address; the reviewed Breez SDK defaults do not prefer Spark over Lightning. [S35] [S34]

For an ordinary receive invoice stored by the operators, one operator can instead look up the invoice or payment hash alongside the owner's Spark identity. Public embedding is unnecessary for that correlation. An operator can then use the identified wallet as the target of refusal on the affected flows. Possessing the invoice alone does not give an outside observer the operator's refusal power. [S13] [S25] [S04]

A merchant or service that already knows Alice can supply the outside information connecting that wallet identity to a person. A public key by itself is not a legal name. Private mode restricts API readers; it does not prevent an operator from disclosing its own database. Proxies may hide a user's direct IP without removing the wallet identity fields. [S32] [S33] [S06] [S13]

Two operators control a threshold, but which secret matters

When A and B withhold their matching signing shares, C alone cannot produce the operator contribution for a new Bitcoin signature. When A and B cooperate, they can supply that contribution. Spark still requires the user contribution: even all three current operator shares alone cannot create an arbitrary Bitcoin spend. [S01] [S07]

A different threshold protects ordinary receive preimages. Two operators with matching stored non-HODL shares can reconstruct the preimage outside the intended release sequence. This can compromise the swap's secret-release condition. Collecting a Lightning payment additionally depends on a party able to use that preimage; this is not a demonstration that SO processes can drain arbitrary Spark balances. [S36] [S37] [S38] [S39]

Former-owner collusion is the conditional theft scenario

Suppose Carol transfers a leaf to Alice. Carol retains her old user key, and operators A and B retain their matching historical shares. Ownership rotation changes the split between the user and operator keys while preserving the combined Bitcoin key. Together, those retained components can form a complete historical signing key without Alice revealing her current key. [S40] [S07]

A fresh conflicting spend is theoretically possible if the relevant output is still spendable and its required ancestry is available. C deleting its old share does not remove the two matching shares retained by A and B. One retained operator share plus Carol's old key alone is insufficient under the stated two-share model. Existing signed stale exits have separate timing and monitoring conditions. [S01] [S40] [S44]

This scenario requires historical retention and the former-owner contribution. If the former owner was an SSP, access to that SSP's corresponding wallet key could supply the latter; control of its SO process alone does not establish that access. Spark includes separate secret storage and deletion mechanisms. Reading the code cannot prove that every historical copy was erased in a deployment. [S41] [S30]

Three operators can remove every operator source of recovery help

All three can withhold every operator copy of recovery records and stop their watchtower assistance. One or two cannot control the copies held by honest survivors, although record completeness and a usable retrieval path still matter. None acquires the power to erase an independent user backup simply by controlling operator servers. [S44] [S42] [S43]

A freeze does not itself invalidate complete, valid, already-signed Bitcoin recovery transactions. Using them still depends on the necessary keys and transaction graph, fee funding, Bitcoin access, timelocks, chain state and monitoring. A seed alone cannot recreate a missing co-signed graph. This technical boundary does not establish that a consumer wallet offers a usable exit flow. [S42] [S43] [S44]

An outage and permanent share loss have different outcomes

If two operators go offline with valid state intact, ordinary signing stops. Users can wait for service to recover; an outage does not force everyone to exit immediately. Even one offline participant can obstruct fresh all-participant preparation. [S07] [S02] [S04]

A wiped metadata database is different from permanent loss of secret shares. Spark separates ordinary state from versioned secret storage. Restoration must recover compatible shares and ownership state; a backup's mere existence does not establish a tested recovery procedure. [S41]

If two matching current shares are permanently lost without recoverable copies, the remaining share cannot sign for the affected existing keys. Starting replacement operators with new keys does not recreate that authority. Users needing access would depend on existing valid Bitcoin recovery paths. If the required graph is also unavailable from every accessible source, funds can become inaccessible. [S07] [S42] [S43]

Reproducible references

Code versions and evidence

Each code link points to the reviewed commit. Official documentation is a dated, mutable reference. The SSP fork is identified separately from official operator code.

  • Spark operator code0b3a32a05c9ac06cc411683551dd1f1bde9d0caa

    Official source; deployed binaries and runtime settings were not verified.

  • Breez Spark SDK61d5b0b494e0c18dd71c2859bed1e946c24b62b1

    Published wallet configuration, preimage sharing, privacy defaults, and recovery-state handling.

  • open-ssp25eec4a8c492a16a4d1962b7115430181a8200ad

    Independent SSP implementation; its existence does not establish compatibility with the founding mainnet operator deployment.

  • open-ssp operator fork83cca565c3cce1a4692cedef601fef553ed0249b

    Fork-specific private RPC permissions; not evidence of the official mainnet permission settings.

  • spark-invoice-doxxerab88dd0b3334d0ff46dbf97c20f838f0f11fb1be

    Public invoice parser; extraction requires an invoice containing the Spark address.

Browse all 45 source references
  1. S01
    Three configured operators; threshold twobreez/spark-sdk · crates/spark-wallet/src/config.rs · lines 52-128

    Defines the mainnet client defaults used by this model.

  2. S02
    All participants versus a threshold subsetbuildonspark/spark · spark/so/helper/operator_helper.go · lines 65-82

    All selects the full configured operator map.

  3. S03
    Every selected result must succeedbuildonspark/spark · spark/so/helper/operator_helper.go · lines 153-178

    A participant error propagates from the helper.

  4. S04
    Preparation failure and rollbackbuildonspark/spark · spark/so/consensus/twopc.go · lines 159-183

    The coordinator prepares locally and across participants; an error aborts this preparation.

  5. S05
    Non-signers still store transfer recordsbuildonspark/spark · spark/so/handler/send_transfer_flow_handler.go · lines 84-89

    The signing subset does not limit which operators persist transfer metadata.

  6. S06
    Sender and recipient fields in transfer statebuildonspark/spark · spark/so/handler/send_transfer_flow_handler.go · lines 158-173

    Shows the identities and leaf-receiver map available in this flow.

  7. S07
    The user contribution plus the operator thresholdbuildonspark/spark · spark/so/helper/signing_coordinator.go · lines 440-487

    Operator signing produces one part of the final signature and selects a threshold of operators.

  8. S08
    Spark send: all-operator consensusbuildonspark/spark · spark/so/handler/transfer_handler_mimo.go · lines 150-155

    SEND_TRANSFER preparation.

  9. S09
    Send API version and request-shape boundarybuildonspark/spark · spark/so/handler/transfer_handler.go · lines 877-888

    V2 dispatch depends on the presence of a transfer package.

  10. S10
    Incoming claim: all-operator consensusbuildonspark/spark · spark/so/handler/claim_transfer_flow_handler.go · lines 175-180

    CLAIM_TRANSFER preparation.

  11. S11
    Deposit finalization: all-operator consensusbuildonspark/spark · spark/so/handler/deposit_handler.go · lines 2044-2077

    FINALIZE_DEPOSIT_TREE preparation.

  12. S12
    Receive preimage storage: all-operator consensusbuildonspark/spark · spark/so/handler/lightning_handler.go · lines 148-185

    StorePreimageShareV2 preparation.

  13. S13
    Each participant checks the wallet and stores invoice metadatabuildonspark/spark · spark/so/handler/preimage_share_flow_handler.go · lines 163-189

    Owner identity, invoice and payment hash are stored alongside that operator's preimage share.

  14. S14
    Lightning swap: all-operator consensusbuildonspark/spark · spark/so/handler/initiate_preimage_swap_flow_handler.go · lines 1081-1094

    InitiatePreimageSwapV3 preparation for the send and receive paths.

  15. S15
    Pending preimage provision: all-operator consensusbuildonspark/spark · spark/so/handler/lightning_handler.go · lines 2241-2247

    PROVIDE_PREIMAGE preparation.

  16. S16
    Pending versus already-committed preimage routingbuildonspark/spark · spark/so/handler/lightning_handler.go · lines 2182-2205

    Some later states bypass fresh preparation.

  17. S17
    Cooperative withdrawal: all-operator consensusbuildonspark/spark · spark/so/handler/coop_exit_handler.go · lines 256-261

    COOP_EXIT preparation.

  18. S18
    Cooperative withdrawal request-shape boundarybuildonspark/spark · spark/so/handler/coop_exit_handler.go · lines 42-63

    The transfer-package branch uses the consensus flow.

  19. S19
    Leaf renewal: all-operator consensusbuildonspark/spark · spark/so/handler/renew_leaf_handler.go · lines 151-167

    RENEW_LEAF preparation.

  20. S20
    Leaf-swap consensus is gatedbuildonspark/spark · spark/so/handler/transfer_handler.go · lines 909-918

    The primary swap branch checks an optional consensus knob.

  21. S21
    Primary leaf-swap preparationbuildonspark/spark · spark/so/handler/swap_primary_transfer_flow_handler.go · lines 643-649

    All operators participate when this branch is selected.

  22. S22
    New static-deposit refund: all-operator consensusbuildonspark/spark · spark/so/handler/static_deposit_utxo_refund_flow_handler.go · lines 647-650

    STATIC_DEPOSIT_UTXO_REFUND preparation.

  23. S23
    Refund re-signing exceptionbuildonspark/spark · spark/so/handler/static_deposit_utxo_refund_flow_handler.go · lines 596-605

    A previously registered refund can take a re-signing path.

  24. S24
    Watchtower-exited leaf recovery and its retry exceptionbuildonspark/spark · spark/so/handler/recover_watchtower_exited_leaf_flow_handler.go · lines 507-550

    First recovery requires all participants; later fee-bump signing can skip the state transition.

  25. S25
    Wallet-specific refusal controlbuildonspark/spark · spark/so/authz/killswitch.go · lines 16-42

    The refusal lookup is keyed by the affected wallet identity.

  26. S26
    Claim kill switch is enforced at the coordinatorbuildonspark/spark · spark/so/handler/claim_transfer_flow_handler.go · lines 95-101

    The source explicitly distinguishes this from a check on every participant.

  27. S27
    Optional Lightning receive-sender allowlistbuildonspark/spark · spark/so/handler/lightning_handler.go · lines 1286-1312

    The gate defaults off and rejects senders not admitted when it is enabled.

  28. S28
    Receive-sender check on every participantbuildonspark/spark · spark/so/handler/initiate_preimage_swap_flow_handler.go · lines 224-242

    Each participant applies the receive-sender gate during preparation.

  29. S29
    Independent SSP calls the same receive-swap APIbenthecarman/open-ssp · src/spark.rs · lines 1054-1085

    open-ssp prepares its liquidity transfer and invokes InitiatePreimageSwapV3.

  30. S30
    Separate SSP wallet, Lightning backend and operator endpointsbenthecarman/open-ssp · src/config.rs · lines 5-48

    Independent service roles and configuration; SO control is not ownership of these separate services.

  31. S31
    Private RPC allowlist in the open-ssp forkbenthecarman/spark · spark/so/grpc/spark_ssp_internal_server.go · lines 45-89

    Applies to the inspected fork when authentication is enabled.

  32. S32
    Private mode is enforced as a server read policybuildonspark/spark · spark/so/handler/wallet_setting_handler.go · lines 196-225

    Restricts ordinary API readers, not the operator's access to its own stored state.

  33. S33
    Read access check for tree queriesbuildonspark/spark · spark/so/handler/tree_query_handler.go · lines 74-94

    The operator applies the wallet privacy policy when serving this query.

  34. S34
    Private mode and invoice preference defaultsbreez/spark-sdk · crates/breez-sdk/core/src/sdk/mod.rs · lines 236-247

    The reviewed SDK defaults to private mode and does not prefer Spark over Lightning.

  35. S35
    Public extraction of an embedded Spark addressbenthecarman/spark-invoice-doxxer · src/main.rs · lines 33-43

    This parser cannot establish that every BOLT11 invoice contains such an address.

  36. S36
    Client splits the preimage into operator sharesbreez/spark-sdk · crates/spark/src/signer/spark_signer_adapter.rs · lines 444-480

    Threshold sharing followed by per-operator encryption.

  37. S37
    Each operator decrypts its own preimage sharebuildonspark/spark · spark/so/handler/preimage_share_flow_handler.go · lines 113-150

    Control of the operator's stored share differs from reconstructing the whole preimage.

  38. S38
    Threshold reconstruction of a receive preimagebuildonspark/spark · spark/so/handler/initiate_preimage_swap_flow_handler.go · lines 768-818

    The honest flow reconstructs from a threshold of participant results.

  39. S39
    HODL swaps without stored preimage sharesbuildonspark/spark · spark/so/handler/initiate_preimage_swap_flow_handler.go · lines 252-260

    Excludes these swaps from the stored-share reconstruction scenario.

  40. S40
    Key rotation preserves the combined leaf keybuildonspark/spark · spark/so/helper/key_tweak_helper.go · lines 124-153

    The new owner public key is derived from the unchanged verifying key and updated operator component.

  41. S41
    Secret-share storage, versions and deletion designbuildonspark/spark · spark/so/entephemeral/README.md

    Separates secret material from ordinary backed-up metadata; source code cannot attest erasure of every historical copy.

  42. S42
    Recovery falls back to persisted wallet statebreez/spark-sdk · crates/spark-wallet/src/wallet.rs · lines 1634-1662

    A failed refresh can use locally stored exit-chain state.

  43. S43
    Incomplete recovery chains cannot be exitedbreez/spark-sdk · crates/breez-sdk/core/src/models/mod.rs · lines 725-737

    Explains the exit-chain data collection requirement.

  44. S44
    Documented stale-owner exit and watchtower obligationsOfficial documentation · checked 2026-09-11

    Mutable official documentation, checked on the review date; not a commit-pinned code source.

  45. S45
    SDK optimization swap claims the incoming counter-transferbreez/spark-sdk · crates/spark/src/services/swap.rs · lines 340-389

    Successful optimization includes claim_transfer on replacement leaves; a deferred claim is not completed availability.