Centralization
Two retained operator shares still require a matching former-owner key to spend
Two matching historical operator shares satisfy the operator side of the published threshold. Spending also requires the matching former-owner key and a still-spendable output with viable ancestry. One other operator deleting its share cannot invalidate those retained keys; retention is not observed. 1234
Content checked against the September 11 source audit. Historical measurements and product searches retain their stated dates; deployed configuration and later releases are not independently attested.
What the evidence shows
Technical and product details
- Documented behavior
Spark's published base configuration sets the Operator threshold to two. The mainnet configuration inherits that value and lists three Operators: Lightspark, Breez, and Flashnet. 1
Supporting material shown on this page
published threshold and three OperatorsPinned source code · github.com · checked 2026-07-14 Relevant lines from the pinned source · open raw file 324 baseUrl: getSspUrl("REGTEST"), 325 identityPublicKey: getSspIdentityPublicKey("REGTEST"), 326 }, 327}; 328 329const MAINNET_WALLET_CONFIG: Required<ConfigOptions> = { 330 ...BASE_CONFIG, - Documented behavior
Operator code describes signing as
1 + (t, n): the user supplies one side andconfig.ThresholdOperators supply the group side. DKG likewise setsMinSignersto the configured threshold. 23Supporting material shown on this page
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 {DKG minimum signersPinned source code · github.com · checked 2026-07-14 Relevant lines from the pinned source · open raw file 45 requestIDString := requestID.String() 46 initRequest := &pbdkg.InitiateDkgRequest{ 47 RequestId: requestIDString, 48 KeyCount: keyCount, 49 MinSigners: config.Threshold, 50 MaxSigners: uint64(len(config.SigningOperatorMap)), 51 CoordinatorIndex: config.Index, - Documented behavior
Any two matching Operator shares therefore satisfy the Operator side of the published default signature; participation by the third Operator is not required. 123
Supporting material shown on this page
published threshold and three OperatorsPinned 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 {DKG minimum signersPinned source code · github.com · checked 2026-07-14 Relevant lines from the pinned source · open raw file 45 requestIDString := requestID.String() 46 initRequest := &pbdkg.InitiateDkgRequest{ 47 RequestId: requestIDString, 48 KeyCount: keyCount, 49 MinSigners: config.Threshold, 50 MaxSigners: uint64(len(config.SigningOperatorMap)), 51 CoordinatorIndex: config.Index,
Interpretation
Why this matters for Spark's trust model
Spark's general research says one honest Operator deleting its old share defeats prior-owner collusion. Under the published two-of-three setting, one honest or nonparticipating Operator cannot veto the other two: preventing reconstruction requires at least two Operators to have deleted or withheld their matching old shares. 1234
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 compares Spark's general trust-model explanation with its published SDK and signing code. It is not an independent attestation of the live servers' threshold, storage, or software, and it is not evidence that any old shares were retained. Operator shares alone do not authorize an arbitrary current Bitcoin spend. The matching user or former-owner contribution, spendable output and applicable ancestry are required. 14
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.
published threshold and three Operators
Pinned source code · github.com · Pinned or archival · checked 2026-07-14
one-plus-threshold signing construction
Pinned source code · github.com · Pinned or archival · checked 2026-07-14
DKG minimum signers
Pinned source code · github.com · Pinned or archival · checked 2026-07-14
Spark's one-honest-Operator explanation
Web source · spark.money · Live source · checked 2026-07-14
How sources, absence findings, and limitations were evaluated