{
  "schema": "spark.operator-capabilities.v1",
  "status": "reviewed",
  "reviewedAt": "2026-09-11",
  "title": "What malicious operators can do",
  "summary": "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.",
  "model": {
    "operatorCount": 3,
    "signingThreshold": 2,
    "description": "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.",
    "assumptions": [
      "An attacker controls the affected operator's service behavior and accessible records and shares. It can refuse requests, disclose its records, or retain historical material.",
      "Control of an SO does not automatically include a user's private key, an independent Spark Service Provider (SSP), its Lightning node, Bitcoin miners, another SO, or the user's backup.",
      "Key thresholds count matching usable shares from the same signing state. Archived copies can change an attacker's effective access; server count alone is not sufficient.",
      "The wallet-operation rows cover the API versions, branches and fresh preparation specified in each row. Older APIs, completed operations and retries can have different requirements. This page covers Bitcoin wallet flows, not Spark token operations."
    ],
    "evidence": [
      "operator-config",
      "signing"
    ]
  },
  "method": "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.",
  "legend": [
    {
      "label": "Yes",
      "meaning": "The stated operator control is sufficient within the row's scope and conditions."
    },
    {
      "label": "No",
      "meaning": "That control alone is insufficient for this action. Read the conditions; this is not a general fund-safety guarantee."
    },
    {
      "label": "Conditional",
      "meaning": "Additional keys, state, identifying information or an optional code path are required."
    },
    {
      "label": "Its / their / all servers",
      "meaning": "Denial is limited to the endpoints the attacker controls."
    }
  ],
  "overview": [
    {
      "operators": 1,
      "title": "Veto preparation; disclose records",
      "text": "A signing minority can still refuse required preparation and publish the metadata stored at its operator.",
      "evidence": [
        "select-all",
        "all-results",
        "replicated-records"
      ],
      "target": "one-operator"
    },
    {
      "operators": 2,
      "title": "Control the operator threshold",
      "text": "One honest operator cannot sign alone. Matching stored receive-preimage shares can also be reconstructed.",
      "evidence": [
        "signing",
        "preimage-reconstruction"
      ],
      "target": "two-operators"
    },
    {
      "operators": 3,
      "title": "Remove all honest operator help",
      "text": "Every operator can withhold service, recovery records and watchtower assistance. Independent user backups remain separate.",
      "evidence": [
        "local-recovery",
        "watchtowers"
      ],
      "target": "three-operators"
    }
  ],
  "sections": [
    {
      "id": "one-operator",
      "title": "One operator can censor without a signing majority",
      "paragraphs": [
        {
          "text": "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.",
          "evidence": [
            "select-all",
            "all-results",
            "prepare",
            "transfer-fields"
          ]
        },
        {
          "text": "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.",
          "evidence": [
            "claim-gate",
            "invoice-records",
            "killswitch"
          ]
        },
        {
          "text": "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.",
          "evidence": [
            "operator-config",
            "select-all",
            "prepare"
          ]
        }
      ]
    },
    {
      "id": "ssp-access",
      "title": "An independent SSP still depends on operator cooperation",
      "paragraphs": [
        {
          "text": "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.",
          "evidence": [
            "ssp-config",
            "open-ssp-receive"
          ]
        },
        {
          "text": "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.",
          "evidence": [
            "ssp-gate",
            "ssp-prepare",
            "flow-swap"
          ]
        },
        {
          "text": "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.",
          "evidence": [
            "ssp-config",
            "ssp-private-rpcs"
          ]
        }
      ]
    },
    {
      "id": "invoice-privacy",
      "title": "An invoice can lead to a wallet identity, then a refusal",
      "paragraphs": [
        {
          "text": "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.",
          "evidence": [
            "invoice-decoding",
            "breez-defaults"
          ]
        },
        {
          "text": "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.",
          "evidence": [
            "invoice-records",
            "killswitch",
            "prepare"
          ]
        },
        {
          "text": "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.",
          "evidence": [
            "private-mode",
            "private-query",
            "transfer-fields",
            "invoice-records"
          ]
        }
      ]
    },
    {
      "id": "two-operators",
      "title": "Two operators control a threshold, but which secret matters",
      "paragraphs": [
        {
          "text": "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.",
          "evidence": [
            "operator-config",
            "signing"
          ]
        },
        {
          "text": "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.",
          "evidence": [
            "preimage-sharing",
            "preimage-decryption",
            "preimage-reconstruction",
            "hodl-exception"
          ]
        }
      ]
    },
    {
      "id": "historical-keys",
      "title": "Former-owner collusion is the conditional theft scenario",
      "paragraphs": [
        {
          "text": "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.",
          "evidence": [
            "key-rotation",
            "signing"
          ]
        },
        {
          "text": "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.",
          "evidence": [
            "operator-config",
            "key-rotation",
            "watchtowers"
          ]
        },
        {
          "text": "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.",
          "evidence": [
            "secret-storage",
            "ssp-config"
          ]
        }
      ]
    },
    {
      "id": "three-operators",
      "title": "Three operators can remove every operator source of recovery help",
      "paragraphs": [
        {
          "text": "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.",
          "evidence": [
            "watchtowers",
            "local-recovery",
            "recovery-requirements"
          ]
        },
        {
          "text": "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.",
          "evidence": [
            "local-recovery",
            "recovery-requirements",
            "watchtowers"
          ]
        }
      ]
    },
    {
      "id": "outage-and-data-loss",
      "title": "An outage and permanent share loss have different outcomes",
      "paragraphs": [
        {
          "text": "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.",
          "evidence": [
            "signing",
            "select-all",
            "prepare"
          ]
        },
        {
          "text": "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.",
          "evidence": [
            "secret-storage"
          ]
        },
        {
          "text": "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.",
          "evidence": [
            "signing",
            "local-recovery",
            "recovery-requirements"
          ]
        }
      ]
    }
  ],
  "repositories": [
    {
      "id": "spark",
      "name": "Spark operator code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "scope": "Official source; deployed binaries and runtime settings were not verified."
    },
    {
      "id": "breez",
      "name": "Breez Spark SDK",
      "repository": "breez/spark-sdk",
      "revision": "61d5b0b494e0c18dd71c2859bed1e946c24b62b1",
      "scope": "Published wallet configuration, preimage sharing, privacy defaults, and recovery-state handling."
    },
    {
      "id": "open-ssp",
      "name": "open-ssp",
      "repository": "benthecarman/open-ssp",
      "revision": "25eec4a8c492a16a4d1962b7115430181a8200ad",
      "scope": "Independent SSP implementation; its existence does not establish compatibility with the founding mainnet operator deployment."
    },
    {
      "id": "ssp-fork",
      "name": "open-ssp operator fork",
      "repository": "benthecarman/spark",
      "revision": "83cca565c3cce1a4692cedef601fef553ed0249b",
      "scope": "Fork-specific private RPC permissions; not evidence of the official mainnet permission settings."
    },
    {
      "id": "invoice-decoder",
      "name": "spark-invoice-doxxer",
      "repository": "benthecarman/spark-invoice-doxxer",
      "revision": "ab88dd0b3334d0ff46dbf97c20f838f0f11fb1be",
      "scope": "Public invoice parser; extraction requires an invoice containing the Spark address."
    }
  ],
  "rows": [
    {
      "category": "Wallet operations",
      "action": "Block a Spark send",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "StartTransferV3; also V2 with a transfer package. Refuse required preparation.",
      "evidence": [
        "flow-send",
        "send-dispatch",
        "prepare"
      ],
      "id": "block-spark-send",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Wallet operations",
      "action": "Block claiming an incoming Spark transfer",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "ClaimTransfer consensus path. A transfer notification is not a completed claim.",
      "evidence": [
        "flow-claim",
        "prepare"
      ],
      "id": "block-transfer-claim",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Wallet operations",
      "action": "Block Bitcoin deposit finalization into Spark",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "FinalizeDepositTreeCreation. This does not prevent the Bitcoin deposit transaction itself.",
      "evidence": [
        "flow-deposit",
        "prepare"
      ],
      "id": "block-deposit-finalization",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Wallet operations",
      "action": "Block normal Lightning receive-invoice setup",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "StorePreimageShareV2 checks the wallet block on every SO during preparation.",
      "evidence": [
        "flow-invoice",
        "invoice-records"
      ],
      "id": "block-invoice-setup",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Wallet operations",
      "action": "Block outgoing Lightning payment initiation",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "InitiatePreimageSwapV3 send path; blocks the Spark funding/swap step.",
      "evidence": [
        "flow-swap",
        "prepare"
      ],
      "id": "block-lightning-send",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Wallet operations",
      "action": "Block incoming Lightning swap settlement into Spark",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "InitiatePreimageSwapV3 receive path. Does not by itself establish that a payer loses money.",
      "evidence": [
        "flow-swap",
        "ssp-prepare"
      ],
      "id": "block-lightning-receive",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Wallet operations",
      "action": "Block completion of a pending HODL swap",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "ProvidePreimage in the pending states that require consensus; some retries skip it.",
      "evidence": [
        "flow-hodl",
        "hodl-routing"
      ],
      "id": "block-hodl-completion",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Wallet operations",
      "action": "Block a cooperative Bitcoin withdrawal",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "CooperativeExitV2 with a transfer package; unilateral exit is a separate path.",
      "evidence": [
        "flow-withdraw",
        "withdraw-dispatch"
      ],
      "id": "block-cooperative-withdrawal",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Wallet operations",
      "action": "Block leaf renewal / timelock refresh",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "RenewLeaf requires every participant.",
      "evidence": [
        "flow-renew",
        "prepare"
      ],
      "id": "block-leaf-renewal",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Wallet operations",
      "action": "Block completion of the reviewed leaf-optimization swap",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "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.",
      "evidence": [
        "sdk-optimization-claim",
        "flow-claim",
        "prepare",
        "optimize-gate",
        "flow-optimize",
        "signing"
      ],
      "id": "block-leaf-optimization",
      "basis": "SDK counter-transfer claim traced to all-participant preparation"
    },
    {
      "category": "Wallet operations",
      "action": "Block a new static-deposit UTXO refund",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "InitiateStaticDepositUtxoRefund consensus path; completed-refund re-signing can differ.",
      "evidence": [
        "flow-refund",
        "refund-retry"
      ],
      "id": "block-static-deposit-refund",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Wallet operations",
      "action": "Block cooperative recovery of a watchtower-exited leaf",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "First RecoverWatchtowerExitedLeaf; already-recovered fee-bump signing skips this state transition.",
      "evidence": [
        "flow-recovery"
      ],
      "id": "block-watchtower-leaf-recovery",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Policy and availability",
      "action": "Condition the affected wallet operations on KYC",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "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.",
      "evidence": [
        "killswitch",
        "invoice-records",
        "prepare"
      ],
      "id": "condition-service-on-kyc",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Policy and availability",
      "action": "Stop new threshold signing by withholding only their own shares",
      "one": "No",
      "two": "Yes",
      "three": "Yes",
      "conditions": "With one refusing SO, two honest shares remain sufficient cryptographically; all-participant preparation is a separate obstacle.",
      "evidence": [
        "operator-config",
        "signing"
      ],
      "id": "withhold-signing-threshold",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Policy and availability",
      "action": "Deny wallet read requests on controlled operator servers",
      "one": "Its server",
      "two": "Their servers",
      "three": "All servers",
      "conditions": "Each controls only its own operator endpoints. An honest survivor can still offer reads; usable wallet failover is a separate implementation question.",
      "evidence": [
        "private-query"
      ],
      "id": "deny-wallet-reads",
      "basis": "Threat-model inference"
    },
    {
      "category": "SSP access",
      "action": "Exclude a competing SSP from normal Lightning receive swaps",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "Official sender allowlist is checked on every participant. Defaults off; production setting unverified.",
      "evidence": [
        "ssp-gate",
        "ssp-prepare",
        "flow-swap"
      ],
      "id": "exclude-competing-ssp",
      "basis": "Code and threshold inference"
    },
    {
      "category": "SSP access",
      "action": "Deny an SSP access to private operator RPCs",
      "one": "Its server",
      "two": "Their servers",
      "three": "All servers",
      "conditions": "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.",
      "evidence": [
        "ssp-private-rpcs"
      ],
      "id": "deny-private-ssp-rpcs",
      "basis": "Threat-model inference"
    },
    {
      "category": "SSP access",
      "action": "Prevent someone from writing or hosting independent SSP software",
      "one": "No",
      "two": "No",
      "three": "No",
      "conditions": "They can deny service on their Spark deployment, not prohibit the software or a different operator set.",
      "evidence": [
        "ssp-config",
        "open-ssp-receive"
      ],
      "id": "prohibit-ssp-software",
      "basis": "Threat-model inference"
    },
    {
      "category": "SSP access",
      "action": "Take over an independent SSP Lightning node solely through SO control",
      "one": "No",
      "two": "No",
      "three": "No",
      "conditions": "Requires additional compromise or control of that separate service.",
      "evidence": [
        "ssp-config"
      ],
      "id": "take-over-ssp-node",
      "basis": "Threat-model inference"
    },
    {
      "category": "SSP access",
      "action": "Stop unrelated Lightning payments outside this Spark deployment",
      "one": "No",
      "two": "No",
      "three": "No",
      "conditions": "SO authority does not confer control over unrelated Lightning nodes.",
      "evidence": [
        "ssp-config"
      ],
      "id": "stop-unrelated-lightning",
      "basis": "Threat-model inference"
    },
    {
      "category": "Privacy",
      "action": "Read available wallet balances and transfer relationships in operator records",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "Applies to records available at the controlled SO; not a guarantee every historical record was retained.",
      "evidence": [
        "replicated-records",
        "transfer-fields"
      ],
      "id": "read-wallet-metadata",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Privacy",
      "action": "Link a stored Lightning invoice/payment hash to a Spark wallet identity",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "Ordinary receive records store these together. Public invoice embedding is unnecessary for operator-side correlation.",
      "evidence": [
        "invoice-records"
      ],
      "id": "link-invoice-to-wallet",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Privacy",
      "action": "Publish operator-held wallet metadata despite private mode",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "Private mode restricts server responses; it does not hide the database from its operator.",
      "evidence": [
        "private-mode",
        "replicated-records"
      ],
      "id": "publish-private-metadata",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Privacy",
      "action": "Log the IP address connecting to the operator",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "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.",
      "evidence": [
        "operator-config"
      ],
      "id": "log-peer-ip",
      "basis": "Threat-model inference"
    },
    {
      "category": "Privacy",
      "action": "Associate a wallet with a real person",
      "one": "Conditional",
      "two": "Conditional",
      "three": "Conditional",
      "conditions": "Requires identifying information, such as a cooperating counterparty or service record. A public key alone is not a legal name.",
      "evidence": [
        "invoice-records"
      ],
      "id": "identify-a-person",
      "basis": "Threat-model inference"
    },
    {
      "category": "Privacy",
      "action": "Extract the public identity from an invoice embedding a Spark address",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "Requires zero malicious SOs: any invoice holder can decode it. Not all invoices embed it.",
      "evidence": [
        "invoice-decoding",
        "breez-defaults"
      ],
      "id": "decode-embedded-identity",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Keys and funds",
      "action": "Reconstruct the operator Bitcoin signing-key component from their shares",
      "one": "No",
      "two": "Yes",
      "three": "Yes",
      "conditions": "Requires matching usable shares. The separate user component is still missing.",
      "evidence": [
        "operator-config",
        "signing"
      ],
      "id": "reconstruct-operator-key",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Keys and funds",
      "action": "Reconstruct a pending receive preimage from their own stored shares",
      "one": "No",
      "two": "Yes",
      "three": "Yes",
      "conditions": "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.",
      "evidence": [
        "preimage-sharing",
        "preimage-decryption",
        "preimage-reconstruction",
        "hodl-exception"
      ],
      "id": "reconstruct-pending-preimage",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Keys and funds",
      "action": "Create an arbitrary Bitcoin spend using only current operator shares",
      "one": "No",
      "two": "No",
      "three": "No",
      "conditions": "A valid corresponding user contribution is still required; specific pre-signed transactions are a different case.",
      "evidence": [
        "signing"
      ],
      "id": "spend-with-current-operator-shares",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Keys and funds",
      "action": "Create a fresh conflicting Bitcoin spend by colluding with a former owner",
      "one": "No",
      "two": "Conditional",
      "three": "Conditional",
      "conditions": "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.",
      "evidence": [
        "signing",
        "key-rotation",
        "secret-storage"
      ],
      "id": "spend-with-former-owner",
      "basis": "Code and threshold inference"
    },
    {
      "category": "Keys and funds",
      "action": "Reassign on-chain Bitcoin ownership merely by editing an operator database",
      "one": "No",
      "two": "No",
      "three": "No",
      "conditions": "Changing a database record does not change Bitcoin spending conditions.",
      "evidence": [
        "signing",
        "key-rotation"
      ],
      "id": "rewrite-bitcoin-ownership",
      "basis": "Threat-model inference"
    },
    {
      "category": "Recovery",
      "action": "Withhold their own operator-held exit records",
      "one": "Yes",
      "two": "Yes",
      "three": "Yes",
      "conditions": "Honest surviving operators or independent backups may still provide the missing data.",
      "evidence": [
        "local-recovery",
        "recovery-requirements"
      ],
      "id": "withhold-own-recovery-records",
      "basis": "Threat-model inference"
    },
    {
      "category": "Recovery",
      "action": "Withhold every operator-held copy of exit records",
      "one": "No",
      "two": "No",
      "three": "Yes",
      "conditions": "One or two cannot control copies held by honest operators. Actual record completeness and retrieval support still matter.",
      "evidence": [
        "watchtowers",
        "local-recovery"
      ],
      "id": "withhold-all-recovery-records",
      "basis": "Threat-model inference"
    },
    {
      "category": "Recovery",
      "action": "Erase an independently stored user recovery backup through SO control alone",
      "one": "No",
      "two": "No",
      "three": "No",
      "conditions": "Would require additional control of the user device or backup service.",
      "evidence": [
        "local-recovery"
      ],
      "id": "erase-user-backup",
      "basis": "Threat-model inference"
    },
    {
      "category": "Recovery",
      "action": "Withdraw all operator-provided watchtower assistance",
      "one": "No",
      "two": "No",
      "three": "Yes",
      "conditions": "Honest operators can continue their assistance; independent watchtowers remain separate.",
      "evidence": [
        "watchtowers"
      ],
      "id": "withdraw-all-watchtowers",
      "basis": "Threat-model inference"
    },
    {
      "category": "Recovery",
      "action": "Invalidate a complete, already-signed Bitcoin exit merely by freezing the wallet",
      "one": "No",
      "two": "No",
      "three": "No",
      "conditions": "Does not remove chain, fee, timelock, monitoring, or conflicting-spend risks.",
      "evidence": [
        "local-recovery",
        "recovery-requirements",
        "watchtowers"
      ],
      "id": "invalidate-signed-exit",
      "basis": "Threat-model inference"
    },
    {
      "category": "Recovery",
      "action": "Destroy the signing threshold by permanently deleting their current shares",
      "one": "No",
      "two": "Yes",
      "three": "Yes",
      "conditions": "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.",
      "evidence": [
        "operator-config",
        "signing",
        "secret-storage"
      ],
      "id": "destroy-signing-threshold",
      "basis": "Code and threshold inference"
    }
  ],
  "sources": [
    {
      "id": "operator-config",
      "title": "Three configured operators; threshold two",
      "kind": "code",
      "repository": "breez/spark-sdk",
      "revision": "61d5b0b494e0c18dd71c2859bed1e946c24b62b1",
      "path": "crates/spark-wallet/src/config.rs",
      "lines": "52-128",
      "note": "Defines the mainnet client defaults used by this model.",
      "url": "https://github.com/breez/spark-sdk/blob/61d5b0b494e0c18dd71c2859bed1e946c24b62b1/crates/spark-wallet/src/config.rs#L52-L128"
    },
    {
      "id": "select-all",
      "title": "All participants versus a threshold subset",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/helper/operator_helper.go",
      "lines": "65-82",
      "note": "All selects the full configured operator map.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/helper/operator_helper.go#L65-L82"
    },
    {
      "id": "all-results",
      "title": "Every selected result must succeed",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/helper/operator_helper.go",
      "lines": "153-178",
      "note": "A participant error propagates from the helper.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/helper/operator_helper.go#L153-L178"
    },
    {
      "id": "prepare",
      "title": "Preparation failure and rollback",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/consensus/twopc.go",
      "lines": "159-183",
      "note": "The coordinator prepares locally and across participants; an error aborts this preparation.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/consensus/twopc.go#L159-L183"
    },
    {
      "id": "replicated-records",
      "title": "Non-signers still store transfer records",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/send_transfer_flow_handler.go",
      "lines": "84-89",
      "note": "The signing subset does not limit which operators persist transfer metadata.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/send_transfer_flow_handler.go#L84-L89"
    },
    {
      "id": "transfer-fields",
      "title": "Sender and recipient fields in transfer state",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/send_transfer_flow_handler.go",
      "lines": "158-173",
      "note": "Shows the identities and leaf-receiver map available in this flow.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/send_transfer_flow_handler.go#L158-L173"
    },
    {
      "id": "signing",
      "title": "The user contribution plus the operator threshold",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/helper/signing_coordinator.go",
      "lines": "440-487",
      "note": "Operator signing produces one part of the final signature and selects a threshold of operators.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/helper/signing_coordinator.go#L440-L487"
    },
    {
      "id": "flow-send",
      "title": "Spark send: all-operator consensus",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/transfer_handler_mimo.go",
      "lines": "150-155",
      "note": "SEND_TRANSFER preparation.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/transfer_handler_mimo.go#L150-L155"
    },
    {
      "id": "send-dispatch",
      "title": "Send API version and request-shape boundary",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/transfer_handler.go",
      "lines": "877-888",
      "note": "V2 dispatch depends on the presence of a transfer package.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/transfer_handler.go#L877-L888"
    },
    {
      "id": "flow-claim",
      "title": "Incoming claim: all-operator consensus",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/claim_transfer_flow_handler.go",
      "lines": "175-180",
      "note": "CLAIM_TRANSFER preparation.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/claim_transfer_flow_handler.go#L175-L180"
    },
    {
      "id": "flow-deposit",
      "title": "Deposit finalization: all-operator consensus",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/deposit_handler.go",
      "lines": "2044-2077",
      "note": "FINALIZE_DEPOSIT_TREE preparation.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/deposit_handler.go#L2044-L2077"
    },
    {
      "id": "flow-invoice",
      "title": "Receive preimage storage: all-operator consensus",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/lightning_handler.go",
      "lines": "148-185",
      "note": "StorePreimageShareV2 preparation.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/lightning_handler.go#L148-L185"
    },
    {
      "id": "invoice-records",
      "title": "Each participant checks the wallet and stores invoice metadata",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/preimage_share_flow_handler.go",
      "lines": "163-189",
      "note": "Owner identity, invoice and payment hash are stored alongside that operator's preimage share.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/preimage_share_flow_handler.go#L163-L189"
    },
    {
      "id": "flow-swap",
      "title": "Lightning swap: all-operator consensus",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/initiate_preimage_swap_flow_handler.go",
      "lines": "1081-1094",
      "note": "InitiatePreimageSwapV3 preparation for the send and receive paths.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/initiate_preimage_swap_flow_handler.go#L1081-L1094"
    },
    {
      "id": "flow-hodl",
      "title": "Pending preimage provision: all-operator consensus",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/lightning_handler.go",
      "lines": "2241-2247",
      "note": "PROVIDE_PREIMAGE preparation.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/lightning_handler.go#L2241-L2247"
    },
    {
      "id": "hodl-routing",
      "title": "Pending versus already-committed preimage routing",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/lightning_handler.go",
      "lines": "2182-2205",
      "note": "Some later states bypass fresh preparation.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/lightning_handler.go#L2182-L2205"
    },
    {
      "id": "flow-withdraw",
      "title": "Cooperative withdrawal: all-operator consensus",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/coop_exit_handler.go",
      "lines": "256-261",
      "note": "COOP_EXIT preparation.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/coop_exit_handler.go#L256-L261"
    },
    {
      "id": "withdraw-dispatch",
      "title": "Cooperative withdrawal request-shape boundary",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/coop_exit_handler.go",
      "lines": "42-63",
      "note": "The transfer-package branch uses the consensus flow.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/coop_exit_handler.go#L42-L63"
    },
    {
      "id": "flow-renew",
      "title": "Leaf renewal: all-operator consensus",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/renew_leaf_handler.go",
      "lines": "151-167",
      "note": "RENEW_LEAF preparation.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/renew_leaf_handler.go#L151-L167"
    },
    {
      "id": "optimize-gate",
      "title": "Leaf-swap consensus is gated",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/transfer_handler.go",
      "lines": "909-918",
      "note": "The primary swap branch checks an optional consensus knob.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/transfer_handler.go#L909-L918"
    },
    {
      "id": "flow-optimize",
      "title": "Primary leaf-swap preparation",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/swap_primary_transfer_flow_handler.go",
      "lines": "643-649",
      "note": "All operators participate when this branch is selected.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/swap_primary_transfer_flow_handler.go#L643-L649"
    },
    {
      "id": "flow-refund",
      "title": "New static-deposit refund: all-operator consensus",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/static_deposit_utxo_refund_flow_handler.go",
      "lines": "647-650",
      "note": "STATIC_DEPOSIT_UTXO_REFUND preparation.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/static_deposit_utxo_refund_flow_handler.go#L647-L650"
    },
    {
      "id": "refund-retry",
      "title": "Refund re-signing exception",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/static_deposit_utxo_refund_flow_handler.go",
      "lines": "596-605",
      "note": "A previously registered refund can take a re-signing path.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/static_deposit_utxo_refund_flow_handler.go#L596-L605"
    },
    {
      "id": "flow-recovery",
      "title": "Watchtower-exited leaf recovery and its retry exception",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/recover_watchtower_exited_leaf_flow_handler.go",
      "lines": "507-550",
      "note": "First recovery requires all participants; later fee-bump signing can skip the state transition.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/recover_watchtower_exited_leaf_flow_handler.go#L507-L550"
    },
    {
      "id": "killswitch",
      "title": "Wallet-specific refusal control",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/authz/killswitch.go",
      "lines": "16-42",
      "note": "The refusal lookup is keyed by the affected wallet identity.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/authz/killswitch.go#L16-L42"
    },
    {
      "id": "claim-gate",
      "title": "Claim kill switch is enforced at the coordinator",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/claim_transfer_flow_handler.go",
      "lines": "95-101",
      "note": "The source explicitly distinguishes this from a check on every participant.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/claim_transfer_flow_handler.go#L95-L101"
    },
    {
      "id": "ssp-gate",
      "title": "Optional Lightning receive-sender allowlist",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/lightning_handler.go",
      "lines": "1286-1312",
      "note": "The gate defaults off and rejects senders not admitted when it is enabled.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/lightning_handler.go#L1286-L1312"
    },
    {
      "id": "ssp-prepare",
      "title": "Receive-sender check on every participant",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/initiate_preimage_swap_flow_handler.go",
      "lines": "224-242",
      "note": "Each participant applies the receive-sender gate during preparation.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/initiate_preimage_swap_flow_handler.go#L224-L242"
    },
    {
      "id": "open-ssp-receive",
      "title": "Independent SSP calls the same receive-swap API",
      "kind": "code",
      "repository": "benthecarman/open-ssp",
      "revision": "25eec4a8c492a16a4d1962b7115430181a8200ad",
      "path": "src/spark.rs",
      "lines": "1054-1085",
      "note": "open-ssp prepares its liquidity transfer and invokes InitiatePreimageSwapV3.",
      "url": "https://github.com/benthecarman/open-ssp/blob/25eec4a8c492a16a4d1962b7115430181a8200ad/src/spark.rs#L1054-L1085"
    },
    {
      "id": "ssp-config",
      "title": "Separate SSP wallet, Lightning backend and operator endpoints",
      "kind": "code",
      "repository": "benthecarman/open-ssp",
      "revision": "25eec4a8c492a16a4d1962b7115430181a8200ad",
      "path": "src/config.rs",
      "lines": "5-48",
      "note": "Independent service roles and configuration; SO control is not ownership of these separate services.",
      "url": "https://github.com/benthecarman/open-ssp/blob/25eec4a8c492a16a4d1962b7115430181a8200ad/src/config.rs#L5-L48"
    },
    {
      "id": "ssp-private-rpcs",
      "title": "Private RPC allowlist in the open-ssp fork",
      "kind": "code",
      "repository": "benthecarman/spark",
      "revision": "83cca565c3cce1a4692cedef601fef553ed0249b",
      "path": "spark/so/grpc/spark_ssp_internal_server.go",
      "lines": "45-89",
      "note": "Applies to the inspected fork when authentication is enabled.",
      "url": "https://github.com/benthecarman/spark/blob/83cca565c3cce1a4692cedef601fef553ed0249b/spark/so/grpc/spark_ssp_internal_server.go#L45-L89"
    },
    {
      "id": "private-mode",
      "title": "Private mode is enforced as a server read policy",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/wallet_setting_handler.go",
      "lines": "196-225",
      "note": "Restricts ordinary API readers, not the operator's access to its own stored state.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/wallet_setting_handler.go#L196-L225"
    },
    {
      "id": "private-query",
      "title": "Read access check for tree queries",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/tree_query_handler.go",
      "lines": "74-94",
      "note": "The operator applies the wallet privacy policy when serving this query.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/tree_query_handler.go#L74-L94"
    },
    {
      "id": "breez-defaults",
      "title": "Private mode and invoice preference defaults",
      "kind": "code",
      "repository": "breez/spark-sdk",
      "revision": "61d5b0b494e0c18dd71c2859bed1e946c24b62b1",
      "path": "crates/breez-sdk/core/src/sdk/mod.rs",
      "lines": "236-247",
      "note": "The reviewed SDK defaults to private mode and does not prefer Spark over Lightning.",
      "url": "https://github.com/breez/spark-sdk/blob/61d5b0b494e0c18dd71c2859bed1e946c24b62b1/crates/breez-sdk/core/src/sdk/mod.rs#L236-L247"
    },
    {
      "id": "invoice-decoding",
      "title": "Public extraction of an embedded Spark address",
      "kind": "code",
      "repository": "benthecarman/spark-invoice-doxxer",
      "revision": "ab88dd0b3334d0ff46dbf97c20f838f0f11fb1be",
      "path": "src/main.rs",
      "lines": "33-43",
      "note": "This parser cannot establish that every BOLT11 invoice contains such an address.",
      "url": "https://github.com/benthecarman/spark-invoice-doxxer/blob/ab88dd0b3334d0ff46dbf97c20f838f0f11fb1be/src/main.rs#L33-L43"
    },
    {
      "id": "preimage-sharing",
      "title": "Client splits the preimage into operator shares",
      "kind": "code",
      "repository": "breez/spark-sdk",
      "revision": "61d5b0b494e0c18dd71c2859bed1e946c24b62b1",
      "path": "crates/spark/src/signer/spark_signer_adapter.rs",
      "lines": "444-480",
      "note": "Threshold sharing followed by per-operator encryption.",
      "url": "https://github.com/breez/spark-sdk/blob/61d5b0b494e0c18dd71c2859bed1e946c24b62b1/crates/spark/src/signer/spark_signer_adapter.rs#L444-L480"
    },
    {
      "id": "preimage-decryption",
      "title": "Each operator decrypts its own preimage share",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/preimage_share_flow_handler.go",
      "lines": "113-150",
      "note": "Control of the operator's stored share differs from reconstructing the whole preimage.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/preimage_share_flow_handler.go#L113-L150"
    },
    {
      "id": "preimage-reconstruction",
      "title": "Threshold reconstruction of a receive preimage",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/initiate_preimage_swap_flow_handler.go",
      "lines": "768-818",
      "note": "The honest flow reconstructs from a threshold of participant results.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/initiate_preimage_swap_flow_handler.go#L768-L818"
    },
    {
      "id": "hodl-exception",
      "title": "HODL swaps without stored preimage shares",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/handler/initiate_preimage_swap_flow_handler.go",
      "lines": "252-260",
      "note": "Excludes these swaps from the stored-share reconstruction scenario.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/handler/initiate_preimage_swap_flow_handler.go#L252-L260"
    },
    {
      "id": "key-rotation",
      "title": "Key rotation preserves the combined leaf key",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/helper/key_tweak_helper.go",
      "lines": "124-153",
      "note": "The new owner public key is derived from the unchanged verifying key and updated operator component.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/helper/key_tweak_helper.go#L124-L153"
    },
    {
      "id": "secret-storage",
      "title": "Secret-share storage, versions and deletion design",
      "kind": "code",
      "repository": "buildonspark/spark",
      "revision": "0b3a32a05c9ac06cc411683551dd1f1bde9d0caa",
      "path": "spark/so/entephemeral/README.md",
      "lines": "",
      "note": "Separates secret material from ordinary backed-up metadata; source code cannot attest erasure of every historical copy.",
      "url": "https://github.com/buildonspark/spark/blob/0b3a32a05c9ac06cc411683551dd1f1bde9d0caa/spark/so/entephemeral/README.md"
    },
    {
      "id": "local-recovery",
      "title": "Recovery falls back to persisted wallet state",
      "kind": "code",
      "repository": "breez/spark-sdk",
      "revision": "61d5b0b494e0c18dd71c2859bed1e946c24b62b1",
      "path": "crates/spark-wallet/src/wallet.rs",
      "lines": "1634-1662",
      "note": "A failed refresh can use locally stored exit-chain state.",
      "url": "https://github.com/breez/spark-sdk/blob/61d5b0b494e0c18dd71c2859bed1e946c24b62b1/crates/spark-wallet/src/wallet.rs#L1634-L1662"
    },
    {
      "id": "recovery-requirements",
      "title": "Incomplete recovery chains cannot be exited",
      "kind": "code",
      "repository": "breez/spark-sdk",
      "revision": "61d5b0b494e0c18dd71c2859bed1e946c24b62b1",
      "path": "crates/breez-sdk/core/src/models/mod.rs",
      "lines": "725-737",
      "note": "Explains the exit-chain data collection requirement.",
      "url": "https://github.com/breez/spark-sdk/blob/61d5b0b494e0c18dd71c2859bed1e946c24b62b1/crates/breez-sdk/core/src/models/mod.rs#L725-L737"
    },
    {
      "id": "watchtowers",
      "title": "Documented stale-owner exit and watchtower obligations",
      "kind": "documentation",
      "url": "https://docs.spark.money/learn/limitations",
      "reviewedAt": "2026-09-11",
      "note": "Mutable official documentation, checked on the review date; not a commit-pinned code source."
    },
    {
      "id": "sdk-optimization-claim",
      "title": "SDK optimization swap claims the incoming counter-transfer",
      "kind": "code",
      "repository": "breez/spark-sdk",
      "revision": "61d5b0b494e0c18dd71c2859bed1e946c24b62b1",
      "path": "crates/spark/src/services/swap.rs",
      "lines": "340-389",
      "note": "Successful optimization includes claim_transfer on replacement leaves; a deferred claim is not completed availability.",
      "url": "https://github.com/breez/spark-sdk/blob/61d5b0b494e0c18dd71c2859bed1e946c24b62b1/crates/spark/src/services/swap.rs#L340-L389"
    }
  ]
}
