From fb635d5fae9bd6dec27afe5a1903f6756882a140 Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Wed, 27 May 2026 18:00:32 -0400 Subject: [PATCH 1/9] feedback: reorganized --- features/networks/spark.mdx | 136 ++++++++++++++++++++++++++---------- 1 file changed, 100 insertions(+), 36 deletions(-) diff --git a/features/networks/spark.mdx b/features/networks/spark.mdx index 289f7170..1d53a083 100644 --- a/features/networks/spark.mdx +++ b/features/networks/spark.mdx @@ -3,67 +3,131 @@ title: "Spark support on Turnkey" sidebarTitle: "Spark" --- -[Spark](https://www.spark.money/) is a Bitcoin Layer 2 network that uses an identity key system for onchain addressing. Turnkey supports Spark address derivation and signing via plain [BIP-340 Schnorr](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) signatures. +[Spark](https://www.spark.money/) is a Bitcoin Layer 2 that uses FROST threshold signing across a collective of operators to enable fast, low-fee transfers and Lightning payments without giving up self-custody of the underlying BTC. Turnkey provides enclave-based key management for Spark: your identity key, leaf keys, deposit keys, and Lightning preimages are generated and used entirely inside the Turnkey enclave, and never leave it during normal operation. -The supported address formats are: +If you're new to the protocol, the [Spark technical overview](https://docs.spark.money/spark/overview) and [Spark core concepts](https://docs.spark.money/spark/key-concepts) are good starting points. This page focuses on what Turnkey adds to a Spark integration. -| Network | Address Format | HRP | -| -------- | -------------------------------- | --------- | -| Mainnet | `ADDRESS_FORMAT_SPARK_MAINNET` | `spark` | -| Regtest | `ADDRESS_FORMAT_SPARK_REGTEST` | `sparkrt` | +## How Spark works (and where Turnkey fits) -### BIP-32 derivation path +Spark *leaves* — the individual units of BTC held inside the protocol — are jointly controlled by your identity key and the Spark Operators using FROST threshold signing. No single party can move a leaf: every leaf operation requires a quorum of operators to co-sign with you. The protocol is designed around a **1-of-n trust assumption**: as long as at least one operator is honest, your funds cannot be stolen. If every operator is unavailable you cannot transact, but you can still exit to Bitcoin L1 using transactions that were pre-signed at deposit time (see [Security model](#security-model)). -Spark uses a unique BIP-32 purpose number (`8797555`) rather than the standard BIP-44 coin type system. The default derivation path for the identity key is: +There are three external roles to know: -``` -m/8797555'/{account}'/0' -``` +- **Spark Operator (SO)** — a node in the operator collective that holds a threshold key share for every leaf. The current operators are Lightspark and Flashnet. +- **Spark Entity (SE)** — the collective of all SOs acting together. A quorum of the SE is required to authorize any leaf operation. +- **Spark Service Provider (SSP)** — an application-layer service (a wallet backend, an exchange, etc.) that coordinates flows on your behalf — routing Lightning payments, processing static deposits, and submitting transfers. The SSP has no key-share authority over leaves; it relies on the SOs for that. -When creating a wallet account via the Turnkey dashboard or API, select `ADDRESS_FORMAT_SPARK_MAINNET` or `ADDRESS_FORMAT_SPARK_REGTEST` and the path will be set automatically. +Turnkey is none of these. Turnkey runs the secure enclave that holds your identity key and derives leaf keys, deposit keys, and Lightning preimages. When a flow needs your signature, the client SDK calls a Turnkey activity (`SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER`, `SPARK_CLAIM_TRANSFER`, or `SPARK_PREPARE_LIGHTNING_RECEIVE`) and the enclave performs the cryptographic work without exposing key material. All communication with SOs and the SSP happens directly from your client — Turnkey is not in that path. - - Only `secp256k1` keys are supported for Spark. Attempting to use an ed25519 key will result in an error. - +{/* Diagram placeholder — actor diagram showing client / Turnkey enclave / SOs / SSP / Bitcoin L1 with arrows labeled by what flows between them. The enclave should be visibly inside Turnkey; SOs and SSP visibly outside. The client should be the only thing that talks to the enclave, the only thing that talks to the SOs and SSP, and the only thing that broadcasts to Bitcoin L1. */} -## Schnorr signing +## Address derivation and key types -Spark transactions are signed using **plain BIP-340 Schnorr** — specifically, without the Taproot key tweak described [here](/features/networks/bitcoin#schnorr-signatures-and-tweaks) that Bitcoin P2TR addresses require. This is an important distinction: passing a Spark address as the `signWith` parameter to `SIGN_RAW_PAYLOAD` triggers plain Schnorr signing, while passing a Bitcoin Taproot (P2TR) address triggers tweaked Schnorr signing per [BIP-341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki). +Spark uses a unique BIP-32 purpose number (`8797555`) rather than the standard BIP-44 coin type system. Every Spark key is a hardened child of `m/8797555'/{account}'`, with the next path segment selecting the key type: -Use the [`SIGN_RAW_PAYLOAD`](/api-reference/activities/sign-raw-payload) or [`SIGN_RAW_PAYLOAD_V2`](/api-reference/activities/sign-raw-payload) activity to sign Spark payloads. The `hashFunction` field should match how the payload was prepared (e.g. `HASH_FUNCTION_NO_OP` if you are passing a pre-hashed payload). +| Type | Path segment | Per-item derivation | Purpose | +|---|---|---|---| +| `IDENTITY` | `/0'` | flat — used as is | Primary wallet identifier; the key behind `ADDRESS_FORMAT_SPARK_*` addresses | +| `SIGNING_HD` | `/1'` | hardened child at `u32_be(sha256(leaf_id)[0..4]) % 2^31` | Base key for per-leaf signing keys | +| `DEPOSIT` | `/2'` | flat — used as is | Single-use L1 deposit address | +| `STATIC_DEPOSIT_HD` | `/3'` | hardened child at `index` | Reusable deposit addresses (SSP integration) | +| `HTLC_PREIMAGE_HD` | `/4'` | not exposed for signing — used only for HMAC-SHA256 inside the enclave | Lightning HTLC preimage generation | -The returned signature will always have `V = "00"` since Schnorr signatures do not use a recovery ID. +The supported `IDENTITY` address formats are: -### Signing scheme selection +| Network | Address format | HRP | +| ------- | -------------- | --- | +| Mainnet | `ADDRESS_FORMAT_SPARK_MAINNET` | `spark` | +| Regtest | `ADDRESS_FORMAT_SPARK_REGTEST` | `sparkrt` | -Turnkey automatically selects the correct signing scheme based on the address format associated with your key: +When creating a wallet account via the Turnkey dashboard or API, select `ADDRESS_FORMAT_SPARK_MAINNET` or `ADDRESS_FORMAT_SPARK_REGTEST` and the identity path will be set automatically. Only `secp256k1` keys are supported; ed25519 keys will be rejected. -| Address type | Signing scheme | -| ------------------- | ---------------------- | -| Bitcoin P2TR | Tweaked Schnorr (BIP-341) | +### Schnorr signing on the identity key + +Spark payloads signed with the identity key use **plain BIP-340 Schnorr** — specifically, *without* the Taproot key tweak described [here](/features/networks/bitcoin#schnorr-signatures-and-tweaks) that Bitcoin P2TR addresses require. Turnkey automatically selects the correct signing scheme based on the address format associated with your key: + +| Address type | Signing scheme | +| ------------ | -------------- | +| Bitcoin P2TR | Tweaked Schnorr (BIP-341) | | Spark Mainnet/Regtest | Plain Schnorr (BIP-340) | -| All others | ECDSA | +| All others | ECDSA | + +Use [`SIGN_RAW_PAYLOAD`](/api-reference/activities/sign-raw-payload) with the Spark address as `signWith` to produce a plain-Schnorr signature. The `hashFunction` field should match how the payload was prepared (e.g. `HASH_FUNCTION_NO_OP` for a pre-hashed payload). The returned signature always has `V = "00"` since Schnorr signatures do not carry a recovery ID. + +This identity-key signing path is sufficient on its own for token operations via the Spark SDK (`@buildonspark/spark-sdk`, `@buildonspark/issuer-sdk`). The FROST-based flows below require the additional Spark-specific activities. + +## Turnkey activities for Spark + +Turnkey exposes four activities purpose-built for Spark flows. All four perform their cryptographic work inside the enclave; no key material is returned to the client. + +| Activity | What it does | +| -------- | ------------ | +| [`SPARK_SIGN_FROST`](/api-reference/activities/sign-frost-spark) | Generates the enclave's FROST signature share for one or more sighashes. Supports batched signing — multiple sighashes in a single call. Used for deposits, withdrawals, transfers, and static deposit claims. | +| [`SPARK_PREPARE_TRANSFER`](/api-reference/activities/prepare-spark-transfer) | Atomically computes per-leaf key tweaks, Feldman-VSS-splits them for the SOs, ECIES-encrypts the recipient's new leaf key to their identity public key, and ECDSA-signs the transfer package with your identity key. Used when sending a transfer. | +| [`SPARK_CLAIM_TRANSFER`](/api-reference/activities/claim-spark-transfer) | Decrypts an inbound transfer's ciphertext using your identity key, derives the receiver's new leaf key, computes the claim tweak, and packages the tweak for the SOs. Used when receiving a transfer. | +| [`SPARK_PREPARE_LIGHTNING_RECEIVE`](/api-reference/activities/spark-prepare-lightning-receive) | Generates a random payment preimage, computes its payment hash, Feldman-splits the preimage across the SOs, and ECIES-encrypts each share to the SO's encryption public key. The raw preimage never leaves the enclave; only the `paymentHash` is returned to the client for BOLT11 invoice construction. | + +These are additive to Turnkey's existing primitives — Spark flows also use [`CREATE_WALLET_ACCOUNTS`](/api-reference/activities/create-wallet-accounts) to derive new deposit and signing keys, [`SIGN_RAW_PAYLOAD`](/api-reference/activities/sign-raw-payload) for Schnorr identity-key signatures, and [`SIGN_TRANSACTION`](/api-reference/activities/sign-transaction) for Bitcoin L1 transactions that fund deposits or receive cooperative withdrawals. + +## Supported operations + +Turnkey supports the full set of Spark wallet operations. For a runnable walkthrough of each flow — including the exact sequence of Turnkey, SO, and SSP calls — see the [SDK example](#sdk-example). + +| Operation | Direction | Turnkey activities used | +| --------- | --------- | ----------------------- | +| Deposit | Bitcoin L1 → Spark (single-use address) | `SIGN_TRANSACTION`, `SPARK_SIGN_FROST` | +| Cooperative withdrawal | Spark → Bitcoin L1 (fast path; requires SO co-signing) | `SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER` | +| Unilateral exit | Spark → Bitcoin L1 (emergency path; no SO cooperation needed) | none at exit time — uses pre-signed transactions from the deposit flow | +| Transfer | Spark → Spark | `SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER` (sender); `SPARK_SIGN_FROST`, `SPARK_CLAIM_TRANSFER` (receiver) | +| Lightning receive | Lightning → Spark | `SPARK_PREPARE_LIGHTNING_RECEIVE` | +| Lightning send | Spark → Lightning | `SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER` | +| Static deposit | Bitcoin L1 → Spark (reusable address) | `CREATE_WALLET_ACCOUNTS`, `EXPORT_WALLET_ACCOUNT`, `SIGN_TRANSACTION` | +| Token transfer | Spark token operations (mint, transfer) | `SIGN_RAW_PAYLOAD` | + +## Security model + +Three things are worth understanding before you integrate. + +### Pre-signed exit transactions are seed-phrase-equivalent + +When you deposit BTC into Spark, the deposit flow pre-signs two Bitcoin L1 transactions inside the Turnkey enclave: a branch transaction and a timelocked exit transaction. These are your unilateral exit path — if every Spark Operator goes offline or acts maliciously, you can broadcast them directly to Bitcoin L1 and recover your BTC without operator cooperation. The wait is approximately 100 blocks (~16 hours) due to the timelock, but the funds are recoverable. + +The corollary: **these transactions must be stored durably.** If they are lost and the SOs are unavailable, your recovery path is gone. Treat them with the same care as a seed phrase — durable, encrypted, off-device storage, with backups. The Turnkey enclave does not retain them; it signs them once at deposit time and returns them to your client. + +This is the property that makes Spark Operator unavailability a *liveness* concern rather than a *safety* concern: you may not be able to transact, but you can always exit. + +### Static deposits export a key from the enclave + +Static deposit addresses are reusable: the same address can receive multiple deposits, each creating a separate Spark leaf. To support this, the SSP must be able to process deposits while your wallet is offline — which means it needs co-signing capability on the static deposit key. This is the **only** Spark flow that uses [`EXPORT_WALLET_ACCOUNT`](/api-reference/activities/export-wallet-account) to take a raw private key out of the Turnkey enclave. + +From the moment the static deposit key is shared with the SSP until you claim any deposits made to that address, the SSP holds co-signing capability. This is the intentional custodial tradeoff of static deposits. To minimize exposure: + +- Use a fresh ephemeral P-256 keypair for each export and zero it immediately after decrypting. +- Transmit the key to the SSP only over an encrypted channel. +- Zero the key in your local memory immediately after transmission. +- Only use SSPs you trust — a compromised SSP holding this key could co-sign spends from the static deposit address. + +If you don't need a reusable receiving address, prefer single-use deposits. They keep key material inside the enclave throughout. -## Networks supported +### Communication with SOs and the SSP is outside Turnkey -- **Spark Mainnet** — `ADDRESS_FORMAT_SPARK_MAINNET` -- **Spark Regtest** — `ADDRESS_FORMAT_SPARK_REGTEST` +Every Spark flow involves direct calls from your client to the Spark Operators (for nonce commitments, partial signatures, leaf state queries, and claim submissions) and, for some flows, to the SSP. Turnkey is not on these network paths and does not see this traffic. Your client SDK is responsible for SO and SSP communication; the Turnkey enclave is responsible for the cryptographic operations on key material. The SDK example shows where each of these responsibilities sits. -## Key features +## SDK example -- **secp256k1 signing**: Turnkey fully supports the secp256k1 curve used by Spark -- **Plain BIP-340 Schnorr**: Distinct from the tweaked Schnorr used for Bitcoin Taproot — no key tweak is applied -- **Bech32m addressing**: Spark identity key addresses are Bech32m-encoded canonical protobuf payloads -- **Spark-specific BIP-32 purpose**: Derivation path uses purpose `8797555` per the Spark protocol spec +The canonical reference for integrating Turnkey with Spark is [`examples/with-spark`](https://github.com/tkhq/sdk/tree/main/examples/with-spark) in the Turnkey SDK monorepo. It contains: -## SDK Example +- A Turnkey-backed Spark signer (`TurnkeySparkSigner`) that plugs into the Spark SDK. +- End-to-end runnable flows for deposit, transfer (send + claim), withdrawal, Lightning receive and send, and static deposit. +- Token-operation scripts (create, mint, transfer) using the issuer SDK. +- Comments mapping each step to the actor model above. -- [`examples/with-spark-schnorr`](https://github.com/tkhq/sdk/tree/main/examples/with-spark-schnorr): demonstrates wallet initialization, SO authentication and token minting + sending on Spark using Turnkey! +If you're integrating Spark, start with the SDK example and refer back to this page for the conceptual model and the security notes. ## Additional resources - [Spark addressing specification](https://docs.spark.money/wallets/addressing) -- [Spark CLI reference](https://docs.spark.money/tools/cli) +- [Spark identity-key derivation](https://docs.spark.money/wallets/identity-key-derivation) - [BIP-340: Schnorr Signatures for secp256k1](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) If you're building on Spark and have questions about integrating with Turnkey, contact us at [hello@turnkey.com](mailto:hello@turnkey.com), on [X](https://x.com/turnkeyhq/), or [on Slack](https://join.slack.com/t/clubturnkey/shared_invite/zt-3aemp2g38-zIh4V~3vNpbX5PsSmkKxcQ). From 34dfdb8faf49fe9089ef1385d34552e2bc8a696c Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Wed, 27 May 2026 18:03:42 -0400 Subject: [PATCH 2/9] feedback: rewording --- features/networks/spark.mdx | 42 ++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/features/networks/spark.mdx b/features/networks/spark.mdx index 1d53a083..a0b30c4b 100644 --- a/features/networks/spark.mdx +++ b/features/networks/spark.mdx @@ -3,21 +3,21 @@ title: "Spark support on Turnkey" sidebarTitle: "Spark" --- -[Spark](https://www.spark.money/) is a Bitcoin Layer 2 that uses FROST threshold signing across a collective of operators to enable fast, low-fee transfers and Lightning payments without giving up self-custody of the underlying BTC. Turnkey provides enclave-based key management for Spark: your identity key, leaf keys, deposit keys, and Lightning preimages are generated and used entirely inside the Turnkey enclave, and never leave it during normal operation. +[Spark](https://www.spark.money/) is a Bitcoin Layer 2 that uses FROST threshold signing across a collective of operators to enable fast, low-fee transfers and Lightning payments without giving up self-custody of the underlying BTC. Turnkey provides enclave-based key management for Spark: your identity key, leaf keys, deposit keys, and Lightning preimages are generated and used inside the Turnkey enclave, and never leave it. -If you're new to the protocol, the [Spark technical overview](https://docs.spark.money/spark/overview) and [Spark core concepts](https://docs.spark.money/spark/key-concepts) are good starting points. This page focuses on what Turnkey adds to a Spark integration. +If you don't know the protocol, read the [Spark technical overview](https://docs.spark.money/spark/overview) and [Spark core concepts](https://docs.spark.money/spark/key-concepts) first. This page covers what Turnkey adds to a Spark integration. ## How Spark works (and where Turnkey fits) -Spark *leaves* — the individual units of BTC held inside the protocol — are jointly controlled by your identity key and the Spark Operators using FROST threshold signing. No single party can move a leaf: every leaf operation requires a quorum of operators to co-sign with you. The protocol is designed around a **1-of-n trust assumption**: as long as at least one operator is honest, your funds cannot be stolen. If every operator is unavailable you cannot transact, but you can still exit to Bitcoin L1 using transactions that were pre-signed at deposit time (see [Security model](#security-model)). +A Spark *leaf* is an individual unit of BTC held inside the protocol. Leaves are jointly controlled by your identity key and the Spark Operators using FROST threshold signing: no single party can move a leaf, and every leaf operation requires a quorum of operators to co-sign with you. The trust model is **1-of-n**: as long as at least one operator is honest, your funds cannot be stolen. If every operator is unavailable you cannot transact, but you can still exit to Bitcoin L1 using transactions pre-signed at deposit time (see [Security model](#security-model)). -There are three external roles to know: +Three external roles to know: -- **Spark Operator (SO)** — a node in the operator collective that holds a threshold key share for every leaf. The current operators are Lightspark and Flashnet. -- **Spark Entity (SE)** — the collective of all SOs acting together. A quorum of the SE is required to authorize any leaf operation. -- **Spark Service Provider (SSP)** — an application-layer service (a wallet backend, an exchange, etc.) that coordinates flows on your behalf — routing Lightning payments, processing static deposits, and submitting transfers. The SSP has no key-share authority over leaves; it relies on the SOs for that. +- **Spark Operator (SO).** A node in the operator collective that holds a threshold key share for every leaf. The current operators are Lightspark and Flashnet. +- **Spark Entity (SE).** The SOs acting together. A quorum of the SE is required to authorize any leaf operation. +- **Spark Service Provider (SSP).** An application-layer service (a wallet backend, an exchange, etc.) that coordinates flows on your behalf: routing Lightning payments, processing static deposits, submitting transfers. The SSP has no key-share authority over leaves; it relies on the SOs for that. -Turnkey is none of these. Turnkey runs the secure enclave that holds your identity key and derives leaf keys, deposit keys, and Lightning preimages. When a flow needs your signature, the client SDK calls a Turnkey activity (`SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER`, `SPARK_CLAIM_TRANSFER`, or `SPARK_PREPARE_LIGHTNING_RECEIVE`) and the enclave performs the cryptographic work without exposing key material. All communication with SOs and the SSP happens directly from your client — Turnkey is not in that path. +Turnkey is none of these. It runs the secure enclave that holds your identity key and derives leaf keys, deposit keys, and Lightning preimages. When a flow needs your signature, the client SDK calls a Turnkey activity (`SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER`, `SPARK_CLAIM_TRANSFER`, or `SPARK_PREPARE_LIGHTNING_RECEIVE`) and the enclave does the cryptographic work without returning key material. All communication with SOs and the SSP happens directly from your client; Turnkey is not on those paths. {/* Diagram placeholder — actor diagram showing client / Turnkey enclave / SOs / SSP / Bitcoin L1 with arrows labeled by what flows between them. The enclave should be visibly inside Turnkey; SOs and SSP visibly outside. The client should be the only thing that talks to the enclave, the only thing that talks to the SOs and SSP, and the only thing that broadcasts to Bitcoin L1. */} @@ -44,7 +44,7 @@ When creating a wallet account via the Turnkey dashboard or API, select `ADDRESS ### Schnorr signing on the identity key -Spark payloads signed with the identity key use **plain BIP-340 Schnorr** — specifically, *without* the Taproot key tweak described [here](/features/networks/bitcoin#schnorr-signatures-and-tweaks) that Bitcoin P2TR addresses require. Turnkey automatically selects the correct signing scheme based on the address format associated with your key: +Spark payloads signed with the identity key use **plain BIP-340 Schnorr**, without the Taproot key tweak that Bitcoin P2TR addresses require (see [Bitcoin Schnorr signatures and tweaks](/features/networks/bitcoin#schnorr-signatures-and-tweaks) for the contrast). Turnkey picks the right signing scheme from the address format on your key: | Address type | Signing scheme | | ------------ | -------------- | @@ -58,26 +58,26 @@ This identity-key signing path is sufficient on its own for token operations via ## Turnkey activities for Spark -Turnkey exposes four activities purpose-built for Spark flows. All four perform their cryptographic work inside the enclave; no key material is returned to the client. +Four activities cover the Spark-specific cryptographic operations. All of them run inside the enclave; none return key material to the client. | Activity | What it does | | -------- | ------------ | -| [`SPARK_SIGN_FROST`](/api-reference/activities/sign-frost-spark) | Generates the enclave's FROST signature share for one or more sighashes. Supports batched signing — multiple sighashes in a single call. Used for deposits, withdrawals, transfers, and static deposit claims. | -| [`SPARK_PREPARE_TRANSFER`](/api-reference/activities/prepare-spark-transfer) | Atomically computes per-leaf key tweaks, Feldman-VSS-splits them for the SOs, ECIES-encrypts the recipient's new leaf key to their identity public key, and ECDSA-signs the transfer package with your identity key. Used when sending a transfer. | -| [`SPARK_CLAIM_TRANSFER`](/api-reference/activities/claim-spark-transfer) | Decrypts an inbound transfer's ciphertext using your identity key, derives the receiver's new leaf key, computes the claim tweak, and packages the tweak for the SOs. Used when receiving a transfer. | -| [`SPARK_PREPARE_LIGHTNING_RECEIVE`](/api-reference/activities/spark-prepare-lightning-receive) | Generates a random payment preimage, computes its payment hash, Feldman-splits the preimage across the SOs, and ECIES-encrypts each share to the SO's encryption public key. The raw preimage never leaves the enclave; only the `paymentHash` is returned to the client for BOLT11 invoice construction. | +| [`SPARK_SIGN_FROST`](/api-reference/activities/sign-frost-spark) | Returns the enclave's FROST signature share for a sighash, or for a batch of them in one call. Used by deposits, withdrawals, transfers, and static deposit claims. | +| [`SPARK_PREPARE_TRANSFER`](/api-reference/activities/prepare-spark-transfer) | Sender side of a transfer. Produces an encrypted transfer package: per-leaf key tweaks Feldman-VSS-split for the SOs, the recipient's new leaf key ECIES-encrypted to their identity public key, and your identity-key ECDSA signature over the whole thing. | +| [`SPARK_CLAIM_TRANSFER`](/api-reference/activities/claim-spark-transfer) | Receiver side of a transfer. Decrypts the inbound leaf-key ciphertext with your identity key, derives the new leaf key, and packages the claim tweak shares for the SOs. | +| [`SPARK_PREPARE_LIGHTNING_RECEIVE`](/api-reference/activities/spark-prepare-lightning-receive) | Returns only the `paymentHash` for a freshly generated Lightning preimage. The preimage itself is created inside the enclave, Feldman-split across the SOs (each share ECIES-encrypted to its operator), and never leaves whole. The hash is what you put in the BOLT11 invoice. | -These are additive to Turnkey's existing primitives — Spark flows also use [`CREATE_WALLET_ACCOUNTS`](/api-reference/activities/create-wallet-accounts) to derive new deposit and signing keys, [`SIGN_RAW_PAYLOAD`](/api-reference/activities/sign-raw-payload) for Schnorr identity-key signatures, and [`SIGN_TRANSACTION`](/api-reference/activities/sign-transaction) for Bitcoin L1 transactions that fund deposits or receive cooperative withdrawals. +These don't replace Turnkey's existing primitives. Spark flows also use [`CREATE_WALLET_ACCOUNTS`](/api-reference/activities/create-wallet-accounts) to derive new deposit and signing keys, [`SIGN_RAW_PAYLOAD`](/api-reference/activities/sign-raw-payload) for identity-key Schnorr signatures, and [`SIGN_TRANSACTION`](/api-reference/activities/sign-transaction) for the Bitcoin L1 transactions that fund deposits or receive cooperative withdrawals. ## Supported operations -Turnkey supports the full set of Spark wallet operations. For a runnable walkthrough of each flow — including the exact sequence of Turnkey, SO, and SSP calls — see the [SDK example](#sdk-example). +Turnkey supports every Spark wallet operation. For a runnable walkthrough of each flow, including the exact sequence of Turnkey, SO, and SSP calls, see the [SDK example](#sdk-example). | Operation | Direction | Turnkey activities used | | --------- | --------- | ----------------------- | | Deposit | Bitcoin L1 → Spark (single-use address) | `SIGN_TRANSACTION`, `SPARK_SIGN_FROST` | | Cooperative withdrawal | Spark → Bitcoin L1 (fast path; requires SO co-signing) | `SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER` | -| Unilateral exit | Spark → Bitcoin L1 (emergency path; no SO cooperation needed) | none at exit time — uses pre-signed transactions from the deposit flow | +| Unilateral exit | Spark → Bitcoin L1 (emergency path; no SO cooperation needed) | none at exit time; uses pre-signed transactions from the deposit flow | | Transfer | Spark → Spark | `SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER` (sender); `SPARK_SIGN_FROST`, `SPARK_CLAIM_TRANSFER` (receiver) | | Lightning receive | Lightning → Spark | `SPARK_PREPARE_LIGHTNING_RECEIVE` | | Lightning send | Spark → Lightning | `SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER` | @@ -86,11 +86,9 @@ Turnkey supports the full set of Spark wallet operations. For a runnable walkthr ## Security model -Three things are worth understanding before you integrate. - ### Pre-signed exit transactions are seed-phrase-equivalent -When you deposit BTC into Spark, the deposit flow pre-signs two Bitcoin L1 transactions inside the Turnkey enclave: a branch transaction and a timelocked exit transaction. These are your unilateral exit path — if every Spark Operator goes offline or acts maliciously, you can broadcast them directly to Bitcoin L1 and recover your BTC without operator cooperation. The wait is approximately 100 blocks (~16 hours) due to the timelock, but the funds are recoverable. +When you deposit BTC into Spark, the deposit flow pre-signs two Bitcoin L1 transactions inside the Turnkey enclave: a branch transaction and a timelocked exit transaction. These are your unilateral exit path. If every Spark Operator goes offline or acts maliciously, you can broadcast them directly to Bitcoin L1 and recover your BTC without operator cooperation. The timelock means the wait is roughly 100 blocks (~16 hours), but the funds are always recoverable. The corollary: **these transactions must be stored durably.** If they are lost and the SOs are unavailable, your recovery path is gone. Treat them with the same care as a seed phrase — durable, encrypted, off-device storage, with backups. The Turnkey enclave does not retain them; it signs them once at deposit time and returns them to your client. @@ -98,14 +96,14 @@ This is the property that makes Spark Operator unavailability a *liveness* conce ### Static deposits export a key from the enclave -Static deposit addresses are reusable: the same address can receive multiple deposits, each creating a separate Spark leaf. To support this, the SSP must be able to process deposits while your wallet is offline — which means it needs co-signing capability on the static deposit key. This is the **only** Spark flow that uses [`EXPORT_WALLET_ACCOUNT`](/api-reference/activities/export-wallet-account) to take a raw private key out of the Turnkey enclave. +Static deposit addresses are reusable: one address can receive many deposits, each creating a separate Spark leaf. To make that work, the SSP needs to process deposits while your wallet is offline, which means it needs co-signing capability on the static deposit key. This is the **only** Spark flow that uses [`EXPORT_WALLET_ACCOUNT`](/api-reference/activities/export-wallet-account) to take a raw private key out of the Turnkey enclave. From the moment the static deposit key is shared with the SSP until you claim any deposits made to that address, the SSP holds co-signing capability. This is the intentional custodial tradeoff of static deposits. To minimize exposure: - Use a fresh ephemeral P-256 keypair for each export and zero it immediately after decrypting. - Transmit the key to the SSP only over an encrypted channel. - Zero the key in your local memory immediately after transmission. -- Only use SSPs you trust — a compromised SSP holding this key could co-sign spends from the static deposit address. +- Only use SSPs you trust: a compromised SSP holding this key can co-sign spends from the static deposit address. If you don't need a reusable receiving address, prefer single-use deposits. They keep key material inside the enclave throughout. From 369cc2e52c65442082921befcff9150a53d78835 Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Wed, 27 May 2026 18:30:56 -0400 Subject: [PATCH 3/9] feedback: add links --- features/networks/spark.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/features/networks/spark.mdx b/features/networks/spark.mdx index a0b30c4b..4f3e8511 100644 --- a/features/networks/spark.mdx +++ b/features/networks/spark.mdx @@ -75,9 +75,9 @@ Turnkey supports every Spark wallet operation. For a runnable walkthrough of eac | Operation | Direction | Turnkey activities used | | --------- | --------- | ----------------------- | -| Deposit | Bitcoin L1 → Spark (single-use address) | `SIGN_TRANSACTION`, `SPARK_SIGN_FROST` | -| Cooperative withdrawal | Spark → Bitcoin L1 (fast path; requires SO co-signing) | `SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER` | -| Unilateral exit | Spark → Bitcoin L1 (emergency path; no SO cooperation needed) | none at exit time; uses pre-signed transactions from the deposit flow | +| Deposit | Bitcoin L1 → Spark (single-use address; also produces the [pre-signed exit transactions](#pre-signed-exit-transactions-are-seed-phrase-equivalent)) | `SIGN_TRANSACTION`, `SPARK_SIGN_FROST` | +| Cooperative withdrawal | Spark → Bitcoin L1 (fast path; requires SO co-signing; falls back to unilateral exit below if SOs are unavailable) | `SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER` | +| Unilateral exit | Spark → Bitcoin L1 (emergency path; no SO cooperation needed) | none at exit time; broadcasts the [pre-signed transactions](#pre-signed-exit-transactions-are-seed-phrase-equivalent) created during deposit | | Transfer | Spark → Spark | `SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER` (sender); `SPARK_SIGN_FROST`, `SPARK_CLAIM_TRANSFER` (receiver) | | Lightning receive | Lightning → Spark | `SPARK_PREPARE_LIGHTNING_RECEIVE` | | Lightning send | Spark → Lightning | `SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER` | From 3c0b95ef5d91048ca11b3b55a0f51ac7c38f7e98 Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Thu, 28 May 2026 10:20:04 -0400 Subject: [PATCH 4/9] feedback: add diagram; minor formatting --- features/networks/spark.mdx | 99 +++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 38 deletions(-) diff --git a/features/networks/spark.mdx b/features/networks/spark.mdx index 4f3e8511..4069e23e 100644 --- a/features/networks/spark.mdx +++ b/features/networks/spark.mdx @@ -9,7 +9,7 @@ If you don't know the protocol, read the [Spark technical overview](https://docs ## How Spark works (and where Turnkey fits) -A Spark *leaf* is an individual unit of BTC held inside the protocol. Leaves are jointly controlled by your identity key and the Spark Operators using FROST threshold signing: no single party can move a leaf, and every leaf operation requires a quorum of operators to co-sign with you. The trust model is **1-of-n**: as long as at least one operator is honest, your funds cannot be stolen. If every operator is unavailable you cannot transact, but you can still exit to Bitcoin L1 using transactions pre-signed at deposit time (see [Security model](#security-model)). +A Spark _leaf_ is an individual unit of BTC held inside the protocol. Leaves are jointly controlled by your identity key and the Spark Operators using FROST threshold signing: no single party can move a leaf, and every leaf operation requires a quorum of operators to co-sign with you. The trust model is **1-of-n**: as long as at least one operator is honest, your funds cannot be stolen. If every operator is unavailable you cannot transact, but you can still exit to Bitcoin L1 using transactions pre-signed at deposit time (see [Security model](#security-model)). Three external roles to know: @@ -17,54 +17,77 @@ Three external roles to know: - **Spark Entity (SE).** The SOs acting together. A quorum of the SE is required to authorize any leaf operation. - **Spark Service Provider (SSP).** An application-layer service (a wallet backend, an exchange, etc.) that coordinates flows on your behalf: routing Lightning payments, processing static deposits, submitting transfers. The SSP has no key-share authority over leaves; it relies on the SOs for that. -Turnkey is none of these. It runs the secure enclave that holds your identity key and derives leaf keys, deposit keys, and Lightning preimages. When a flow needs your signature, the client SDK calls a Turnkey activity (`SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER`, `SPARK_CLAIM_TRANSFER`, or `SPARK_PREPARE_LIGHTNING_RECEIVE`) and the enclave does the cryptographic work without returning key material. All communication with SOs and the SSP happens directly from your client; Turnkey is not on those paths. +Turnkey is none of these. It runs the secure enclave that holds your identity key and derives leaf keys, deposit keys, and Lightning preimages. When a flow needs your signature, the client calls a Turnkey activity ([`SPARK_SIGN_FROST`](/api-reference/activities/sign-frost-spark), [`SPARK_PREPARE_TRANSFER`](/api-reference/activities/prepare-spark-transfer), [`SPARK_CLAIM_TRANSFER`](/api-reference/activities/claim-spark-transfer), or [`SPARK_PREPARE_LIGHTNING_RECEIVE`](/api-reference/activities/spark-prepare-lightning-receive)) and the enclave does the cryptographic work without returning key material. All communication with SOs and the SSP happens directly from the client; Turnkey is not on those paths. -{/* Diagram placeholder — actor diagram showing client / Turnkey enclave / SOs / SSP / Bitcoin L1 with arrows labeled by what flows between them. The enclave should be visibly inside Turnkey; SOs and SSP visibly outside. The client should be the only thing that talks to the enclave, the only thing that talks to the SOs and SSP, and the only thing that broadcasts to Bitcoin L1. */} +```mermaid +graph LR + Client["Your client SDK"] + + subgraph TK["Turnkey"] + Enclave["Secure enclave
holds identity, leaf,
deposit, and preimage keys"] + end + + subgraph SE["Spark Entity (SE)"] + SOs["Spark Operators
(threshold key shares)"] + end + + SSP["Spark Service Provider"] + L1["Bitcoin L1"] + + Client <-->|"SPARK_* activities,
CREATE_WALLET_ACCOUNTS,
SIGN_RAW_PAYLOAD,
SIGN_TRANSACTION"| Enclave + Client <-->|"FROST nonces & partial sigs,
leaf state, claim submissions"| SOs + Client <-->|"transfer routing,
Lightning, static deposits"| SSP + Client -->|"L1 broadcasts
(deposit, withdrawal, unilateral exit)"| L1 +``` + +Turnkey only connects to the client. SO, SSP, and L1 communication happens directly from your client; Turnkey is not on those paths. ## Address derivation and key types Spark uses a unique BIP-32 purpose number (`8797555`) rather than the standard BIP-44 coin type system. Every Spark key is a hardened child of `m/8797555'/{account}'`, with the next path segment selecting the key type: -| Type | Path segment | Per-item derivation | Purpose | -|---|---|---|---| -| `IDENTITY` | `/0'` | flat — used as is | Primary wallet identifier; the key behind `ADDRESS_FORMAT_SPARK_*` addresses | -| `SIGNING_HD` | `/1'` | hardened child at `u32_be(sha256(leaf_id)[0..4]) % 2^31` | Base key for per-leaf signing keys | -| `DEPOSIT` | `/2'` | flat — used as is | Single-use L1 deposit address | -| `STATIC_DEPOSIT_HD` | `/3'` | hardened child at `index` | Reusable deposit addresses (SSP integration) | -| `HTLC_PREIMAGE_HD` | `/4'` | not exposed for signing — used only for HMAC-SHA256 inside the enclave | Lightning HTLC preimage generation | +| Type | Path segment | Per-item derivation | Purpose | +| ------------------- | ------------ | --------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| `IDENTITY` | `/0'` | flat (used as is) | Primary wallet identifier; the key behind `ADDRESS_FORMAT_SPARK_*` addresses | +| `SIGNING_HD` | `/1'` | hardened child at `u32_be(sha256(leaf_id)[0..4]) % 2^31` | Base key for per-leaf signing keys | +| `DEPOSIT` | `/2'` | flat (used as is) | Single-use L1 deposit address | +| `STATIC_DEPOSIT_HD` | `/3'` | hardened child at `index` | Reusable deposit addresses (SSP integration) | +| `HTLC_PREIMAGE_HD` | `/4'` | not exposed for signing; used only for HMAC-SHA256 inside the enclave | Lightning HTLC preimage generation | The supported `IDENTITY` address formats are: -| Network | Address format | HRP | -| ------- | -------------- | --- | -| Mainnet | `ADDRESS_FORMAT_SPARK_MAINNET` | `spark` | +| Network | Address format | HRP | +| ------- | ------------------------------ | --------- | +| Mainnet | `ADDRESS_FORMAT_SPARK_MAINNET` | `spark` | | Regtest | `ADDRESS_FORMAT_SPARK_REGTEST` | `sparkrt` | -When creating a wallet account via the Turnkey dashboard or API, select `ADDRESS_FORMAT_SPARK_MAINNET` or `ADDRESS_FORMAT_SPARK_REGTEST` and the identity path will be set automatically. Only `secp256k1` keys are supported; ed25519 keys will be rejected. +When creating a wallet account via the Turnkey dashboard or API, select `ADDRESS_FORMAT_SPARK_MAINNET` or `ADDRESS_FORMAT_SPARK_REGTEST` and the identity path will be set automatically. Only `secp256k1` keys are supported; `ed25519` keys will be rejected. ### Schnorr signing on the identity key Spark payloads signed with the identity key use **plain BIP-340 Schnorr**, without the Taproot key tweak that Bitcoin P2TR addresses require (see [Bitcoin Schnorr signatures and tweaks](/features/networks/bitcoin#schnorr-signatures-and-tweaks) for the contrast). Turnkey picks the right signing scheme from the address format on your key: -| Address type | Signing scheme | -| ------------ | -------------- | -| Bitcoin P2TR | Tweaked Schnorr (BIP-341) | -| Spark Mainnet/Regtest | Plain Schnorr (BIP-340) | -| All others | ECDSA | +| Address type | Signing scheme | +| --------------------- | ------------------------- | +| Bitcoin P2TR | Tweaked Schnorr (BIP-341) | +| Spark Mainnet/Regtest | Plain Schnorr (BIP-340) | +| All others | ECDSA | Use [`SIGN_RAW_PAYLOAD`](/api-reference/activities/sign-raw-payload) with the Spark address as `signWith` to produce a plain-Schnorr signature. The `hashFunction` field should match how the payload was prepared (e.g. `HASH_FUNCTION_NO_OP` for a pre-hashed payload). The returned signature always has `V = "00"` since Schnorr signatures do not carry a recovery ID. This identity-key signing path is sufficient on its own for token operations via the Spark SDK (`@buildonspark/spark-sdk`, `@buildonspark/issuer-sdk`). The FROST-based flows below require the additional Spark-specific activities. +For more information on Spark keys and address derivation, see documentation [here](https://docs.spark.money/wallets/identity-key-derivation). + ## Turnkey activities for Spark Four activities cover the Spark-specific cryptographic operations. All of them run inside the enclave; none return key material to the client. -| Activity | What it does | -| -------- | ------------ | -| [`SPARK_SIGN_FROST`](/api-reference/activities/sign-frost-spark) | Returns the enclave's FROST signature share for a sighash, or for a batch of them in one call. Used by deposits, withdrawals, transfers, and static deposit claims. | -| [`SPARK_PREPARE_TRANSFER`](/api-reference/activities/prepare-spark-transfer) | Sender side of a transfer. Produces an encrypted transfer package: per-leaf key tweaks Feldman-VSS-split for the SOs, the recipient's new leaf key ECIES-encrypted to their identity public key, and your identity-key ECDSA signature over the whole thing. | -| [`SPARK_CLAIM_TRANSFER`](/api-reference/activities/claim-spark-transfer) | Receiver side of a transfer. Decrypts the inbound leaf-key ciphertext with your identity key, derives the new leaf key, and packages the claim tweak shares for the SOs. | +| Activity | What it does | +| ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`SPARK_SIGN_FROST`](/api-reference/activities/sign-frost-spark) | Returns the enclave's FROST signature share for a sighash, or for a batch of them in one call. Used by deposits, withdrawals, transfers, and static deposit claims. | +| [`SPARK_PREPARE_TRANSFER`](/api-reference/activities/prepare-spark-transfer) | Sender side of a transfer. Produces an encrypted transfer package: per-leaf key tweaks Feldman-VSS-split for the SOs, the recipient's new leaf key ECIES-encrypted to their identity public key, and your identity-key ECDSA signature over the whole thing. | +| [`SPARK_CLAIM_TRANSFER`](/api-reference/activities/claim-spark-transfer) | Receiver side of a transfer. Decrypts the inbound leaf-key ciphertext with your identity key, derives the new leaf key, and packages the claim tweak shares for the SOs. | | [`SPARK_PREPARE_LIGHTNING_RECEIVE`](/api-reference/activities/spark-prepare-lightning-receive) | Returns only the `paymentHash` for a freshly generated Lightning preimage. The preimage itself is created inside the enclave, Feldman-split across the SOs (each share ECIES-encrypted to its operator), and never leaves whole. The hash is what you put in the BOLT11 invoice. | These don't replace Turnkey's existing primitives. Spark flows also use [`CREATE_WALLET_ACCOUNTS`](/api-reference/activities/create-wallet-accounts) to derive new deposit and signing keys, [`SIGN_RAW_PAYLOAD`](/api-reference/activities/sign-raw-payload) for identity-key Schnorr signatures, and [`SIGN_TRANSACTION`](/api-reference/activities/sign-transaction) for the Bitcoin L1 transactions that fund deposits or receive cooperative withdrawals. @@ -73,26 +96,26 @@ These don't replace Turnkey's existing primitives. Spark flows also use [`CREATE Turnkey supports every Spark wallet operation. For a runnable walkthrough of each flow, including the exact sequence of Turnkey, SO, and SSP calls, see the [SDK example](#sdk-example). -| Operation | Direction | Turnkey activities used | -| --------- | --------- | ----------------------- | -| Deposit | Bitcoin L1 → Spark (single-use address; also produces the [pre-signed exit transactions](#pre-signed-exit-transactions-are-seed-phrase-equivalent)) | `SIGN_TRANSACTION`, `SPARK_SIGN_FROST` | -| Cooperative withdrawal | Spark → Bitcoin L1 (fast path; requires SO co-signing; falls back to unilateral exit below if SOs are unavailable) | `SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER` | -| Unilateral exit | Spark → Bitcoin L1 (emergency path; no SO cooperation needed) | none at exit time; broadcasts the [pre-signed transactions](#pre-signed-exit-transactions-are-seed-phrase-equivalent) created during deposit | -| Transfer | Spark → Spark | `SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER` (sender); `SPARK_SIGN_FROST`, `SPARK_CLAIM_TRANSFER` (receiver) | -| Lightning receive | Lightning → Spark | `SPARK_PREPARE_LIGHTNING_RECEIVE` | -| Lightning send | Spark → Lightning | `SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER` | -| Static deposit | Bitcoin L1 → Spark (reusable address) | `CREATE_WALLET_ACCOUNTS`, `EXPORT_WALLET_ACCOUNT`, `SIGN_TRANSACTION` | -| Token transfer | Spark token operations (mint, transfer) | `SIGN_RAW_PAYLOAD` | +| Operation | Direction | Turnkey activities used | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| Deposit | Bitcoin L1 → Spark (single-use address; also produces the [pre-signed exit transactions](#pre-signed-exit-transactions-are-seed-phrase-equivalent)) | `SIGN_TRANSACTION`, `SPARK_SIGN_FROST` | +| Cooperative withdrawal | Spark → Bitcoin L1 (fast path; requires SO co-signing; falls back to unilateral exit below if SOs are unavailable) | `SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER` | +| Unilateral exit | Spark → Bitcoin L1 (emergency path; no SO cooperation needed) | none at exit time; broadcasts the [pre-signed transactions](#pre-signed-exit-transactions-are-seed-phrase-equivalent) created during deposit | +| Transfer | Spark → Spark | `SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER` (sender); `SPARK_SIGN_FROST`, `SPARK_CLAIM_TRANSFER` (receiver) | +| Lightning receive | Lightning → Spark | `SPARK_PREPARE_LIGHTNING_RECEIVE` | +| Lightning send | Spark → Lightning | `SPARK_SIGN_FROST`, `SPARK_PREPARE_TRANSFER` | +| Static deposit | Bitcoin L1 → Spark (reusable address) | `CREATE_WALLET_ACCOUNTS`, `EXPORT_WALLET_ACCOUNT`, `SIGN_TRANSACTION` | +| Token transfer | Spark token operations (mint, transfer) | `SIGN_RAW_PAYLOAD` | ## Security model ### Pre-signed exit transactions are seed-phrase-equivalent -When you deposit BTC into Spark, the deposit flow pre-signs two Bitcoin L1 transactions inside the Turnkey enclave: a branch transaction and a timelocked exit transaction. These are your unilateral exit path. If every Spark Operator goes offline or acts maliciously, you can broadcast them directly to Bitcoin L1 and recover your BTC without operator cooperation. The timelock means the wait is roughly 100 blocks (~16 hours), but the funds are always recoverable. +When you deposit BTC into Spark, the deposit flow pre-signs two Bitcoin L1 transactions inside the Turnkey enclave: a branch transaction and a timelocked exit transaction. These are your unilateral exit path. If every Spark Operator goes offline or acts maliciously, you can broadcast them directly to Bitcoin L1 and recover your BTC without operator cooperation. Per the [Spark sovereignty docs](https://docs.spark.money/learn/sovereignty), exiting can take "as little as 100 blocks" (~16 hours) — the actual wait depends on leaf depth and how recently the leaf was transferred, since timelocks decrement at each transfer. -The corollary: **these transactions must be stored durably.** If they are lost and the SOs are unavailable, your recovery path is gone. Treat them with the same care as a seed phrase — durable, encrypted, off-device storage, with backups. The Turnkey enclave does not retain them; it signs them once at deposit time and returns them to your client. +The corollary: **these transactions must be stored durably.** If they are lost and the SOs are unavailable, your recovery path is gone. Treat them with the same care as a seed phrase — durable, encrypted, off-device storage, with backups. The Turnkey enclave does not retain them; it signs them once at deposit time and returns them to the client. -This is the property that makes Spark Operator unavailability a *liveness* concern rather than a *safety* concern: you may not be able to transact, but you can always exit. +This is the property that makes Spark Operator unavailability a _liveness_ concern rather than a _safety_ concern: you may not be able to transact, but you can always exit. ### Static deposits export a key from the enclave @@ -109,7 +132,7 @@ If you don't need a reusable receiving address, prefer single-use deposits. They ### Communication with SOs and the SSP is outside Turnkey -Every Spark flow involves direct calls from your client to the Spark Operators (for nonce commitments, partial signatures, leaf state queries, and claim submissions) and, for some flows, to the SSP. Turnkey is not on these network paths and does not see this traffic. Your client SDK is responsible for SO and SSP communication; the Turnkey enclave is responsible for the cryptographic operations on key material. The SDK example shows where each of these responsibilities sits. +Every Spark flow involves direct calls from the client to the Spark Operators (for nonce commitments, partial signatures, leaf state queries, and claim submissions) and, for some flows, to the SSP. Turnkey is not on these network paths and does not see this traffic. The client SDK is responsible for SO and SSP communication; the Turnkey enclave is responsible for the cryptographic operations on key material. The SDK example shows where each of these responsibilities sits. ## SDK example From ce84355490007c06849f0ff5476fc1baeb6f5c27 Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Thu, 28 May 2026 10:30:37 -0400 Subject: [PATCH 5/9] fix links and activity-related commentary --- features/networks/spark.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/networks/spark.mdx b/features/networks/spark.mdx index 4069e23e..dd68db87 100644 --- a/features/networks/spark.mdx +++ b/features/networks/spark.mdx @@ -5,7 +5,7 @@ sidebarTitle: "Spark" [Spark](https://www.spark.money/) is a Bitcoin Layer 2 that uses FROST threshold signing across a collective of operators to enable fast, low-fee transfers and Lightning payments without giving up self-custody of the underlying BTC. Turnkey provides enclave-based key management for Spark: your identity key, leaf keys, deposit keys, and Lightning preimages are generated and used inside the Turnkey enclave, and never leave it. -If you don't know the protocol, read the [Spark technical overview](https://docs.spark.money/spark/overview) and [Spark core concepts](https://docs.spark.money/spark/key-concepts) first. This page covers what Turnkey adds to a Spark integration. +If you don't know the protocol, read [Spark core concepts](https://docs.spark.money/learn/core-concepts) and [Sovereignty](https://docs.spark.money/learn/sovereignty) first. This page covers what Turnkey adds to a Spark integration. ## How Spark works (and where Turnkey fits) @@ -90,7 +90,7 @@ Four activities cover the Spark-specific cryptographic operations. All of them r | [`SPARK_CLAIM_TRANSFER`](/api-reference/activities/claim-spark-transfer) | Receiver side of a transfer. Decrypts the inbound leaf-key ciphertext with your identity key, derives the new leaf key, and packages the claim tweak shares for the SOs. | | [`SPARK_PREPARE_LIGHTNING_RECEIVE`](/api-reference/activities/spark-prepare-lightning-receive) | Returns only the `paymentHash` for a freshly generated Lightning preimage. The preimage itself is created inside the enclave, Feldman-split across the SOs (each share ECIES-encrypted to its operator), and never leaves whole. The hash is what you put in the BOLT11 invoice. | -These don't replace Turnkey's existing primitives. Spark flows also use [`CREATE_WALLET_ACCOUNTS`](/api-reference/activities/create-wallet-accounts) to derive new deposit and signing keys, [`SIGN_RAW_PAYLOAD`](/api-reference/activities/sign-raw-payload) for identity-key Schnorr signatures, and [`SIGN_TRANSACTION`](/api-reference/activities/sign-transaction) for the Bitcoin L1 transactions that fund deposits or receive cooperative withdrawals. +These don't replace Turnkey's existing primitives. Spark flows also use [`CREATE_WALLET_ACCOUNTS`](/api-reference/activities/create-wallet-accounts) to derive new deposit and signing keys, [`SIGN_RAW_PAYLOAD`](/api-reference/activities/sign-raw-payload) for identity-key Schnorr signatures, [`SIGN_TRANSACTION`](/api-reference/activities/sign-transaction) for the Bitcoin L1 transactions that fund deposits or receive cooperative withdrawals, and [`EXPORT_WALLET_ACCOUNT`](/api-reference/activities/export-wallet-account) in exactly one flow — see [Static deposits export a key from the enclave](#static-deposits-export-a-key-from-the-enclave). ## Supported operations From d2ac18125a9092040c1fc691ed2fc3282056686e Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Thu, 28 May 2026 11:00:31 -0400 Subject: [PATCH 6/9] updated diagram --- features/networks/spark.mdx | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/features/networks/spark.mdx b/features/networks/spark.mdx index dd68db87..9c90eff3 100644 --- a/features/networks/spark.mdx +++ b/features/networks/spark.mdx @@ -21,26 +21,26 @@ Turnkey is none of these. It runs the secure enclave that holds your identity ke ```mermaid graph LR - Client["Your client SDK"] - - subgraph TK["Turnkey"] - Enclave["Secure enclave
holds identity, leaf,
deposit, and preimage keys"] + subgraph TK["Inside Turnkey"] + E["Secure enclave
• holds your keys
• signs (FROST, Schnorr, ECDSA)
• encrypts operator packages
• derives HD subkeys"] end - subgraph SE["Spark Entity (SE)"] - SOs["Spark Operators
(threshold key shares)"] - end + C["Client SDK
(orchestrator)"] - SSP["Spark Service Provider"] - L1["Bitcoin L1"] + subgraph SP["External Spark + L1"] + direction TB + SO["Spark Operators (SE)"] + SSP["Spark Service Provider"] + L1["Bitcoin L1"] + end - Client <-->|"SPARK_* activities,
CREATE_WALLET_ACCOUNTS,
SIGN_RAW_PAYLOAD,
SIGN_TRANSACTION"| Enclave - Client <-->|"FROST nonces & partial sigs,
leaf state, claim submissions"| SOs - Client <-->|"transfer routing,
Lightning, static deposits"| SSP - Client -->|"L1 broadcasts
(deposit, withdrawal, unilateral exit)"| L1 + E <-->|"signature shares,
encrypted packages"| C + C <-->|"FROST + leaf state"| SO + C <-->|"Lightning, transfers,
static deposits"| SSP + C -->|"signed Bitcoin txs"| L1 ``` -Turnkey only connects to the client. SO, SSP, and L1 communication happens directly from your client; Turnkey is not on those paths. +Turnkey only connects to the client. SO, SSP, and L1 communication happens directly from the client; Turnkey is not on those paths. ## Address derivation and key types From 3552c0f8ae56dec77e001d83c5ba9bb86f860136 Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Tue, 2 Jun 2026 13:26:12 +0900 Subject: [PATCH 7/9] feedback: caveat static-deposit key export Address review on static deposit security caveats: - Flag the static-deposit key-export exception in the top-of-page caveat and link to the security section - Wrap the EXPORT_WALLET_ACCOUNT note in a callout box - Add reassurance that the exported key is irrelevant after claiming (links SPARK_CLAIM_TRANSFER + SDK example) - Outline the SSP + Spark Operator collusion risk window and link Spark's sovereignty docs --- features/networks/spark.mdx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/features/networks/spark.mdx b/features/networks/spark.mdx index 9c90eff3..07f74cf6 100644 --- a/features/networks/spark.mdx +++ b/features/networks/spark.mdx @@ -3,7 +3,7 @@ title: "Spark support on Turnkey" sidebarTitle: "Spark" --- -[Spark](https://www.spark.money/) is a Bitcoin Layer 2 that uses FROST threshold signing across a collective of operators to enable fast, low-fee transfers and Lightning payments without giving up self-custody of the underlying BTC. Turnkey provides enclave-based key management for Spark: your identity key, leaf keys, deposit keys, and Lightning preimages are generated and used inside the Turnkey enclave, and never leave it. +[Spark](https://www.spark.money/) is a Bitcoin Layer 2 that uses FROST threshold signing across a collective of operators to enable fast, low-fee transfers and Lightning payments without giving up self-custody of the underlying BTC. Turnkey provides enclave-based key management for Spark: your identity key, leaf keys, deposit keys, and Lightning preimages are generated and used inside the Turnkey enclave. None of this key material ever leaves it, with the lone exception being the [static deposit flow](#static-deposits-export-a-key-from-the-enclave). This flow, by necessity, exports one deposit key so a Spark Service Provider can process deposits while your wallet is offline, and that key stops mattering once you claim the deposit. If you don't know the protocol, read [Spark core concepts](https://docs.spark.money/learn/core-concepts) and [Sovereignty](https://docs.spark.money/learn/sovereignty) first. This page covers what Turnkey adds to a Spark integration. @@ -119,14 +119,24 @@ This is the property that makes Spark Operator unavailability a _liveness_ conce ### Static deposits export a key from the enclave -Static deposit addresses are reusable: one address can receive many deposits, each creating a separate Spark leaf. To make that work, the SSP needs to process deposits while your wallet is offline, which means it needs co-signing capability on the static deposit key. This is the **only** Spark flow that uses [`EXPORT_WALLET_ACCOUNT`](/api-reference/activities/export-wallet-account) to take a raw private key out of the Turnkey enclave. +Static deposit addresses are reusable: one address can receive many deposits, each creating a separate Spark leaf. To make that work, the SSP needs to process deposits while your wallet is offline, which means it needs co-signing capability on the static deposit key. -From the moment the static deposit key is shared with the SSP until you claim any deposits made to that address, the SSP holds co-signing capability. This is the intentional custodial tradeoff of static deposits. To minimize exposure: + + Static deposits are the **only** Spark flow that takes a raw private key out of the Turnkey enclave. The flow uses [`EXPORT_WALLET_ACCOUNT`](/api-reference/activities/export-wallet-account) to export the static deposit key so it can be shared with the SSP. Every other Spark flow keeps all key material inside the enclave. + + +This is the intentional custodial tradeoff of static deposits — expected behavior, not a leak. The exported key controls only the **static deposit address**; it cannot move existing leaves or touch your identity key. To minimize exposure while the key is in transit: - Use a fresh ephemeral P-256 keypair for each export and zero it immediately after decrypting. - Transmit the key to the SSP only over an encrypted channel. - Zero the key in your local memory immediately after transmission. -- Only use SSPs you trust: a compromised SSP holding this key can co-sign spends from the static deposit address. + + + **The exported key stops mattering once you claim the deposit.** Claiming runs through [`SPARK_CLAIM_TRANSFER`](/api-reference/activities/claim-spark-transfer), which rotates the leaf to a fresh key derived inside your enclave; after that, the exported static deposit key has no authority over the funds. + {/* TODO: See the static deposit scripts in the [SDK example](#sdk-example) for the full claim flow. */} + + +**The risk window.** A static deposit address is an aggregate of your static deposit key and the Spark Operators' key, so moving funds out of it requires a signature from both. Between the moment funds arrive at the address and the moment you claim them, the SSP holds your half of that key. During this window — and only this window — an SSP that **colludes with the Spark Operators** could co-sign a spend of the unclaimed deposit. This is the same operator-trust boundary described in Spark's [sovereignty model](https://docs.spark.money/learn/sovereignty); use only SSPs you trust, and claim deposits promptly to keep the window short. If you don't need a reusable receiving address, prefer single-use deposits. They keep key material inside the enclave throughout. From fd3242705193458a37449693dd6da2d32d1fa1e4 Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Wed, 3 Jun 2026 14:37:52 +0900 Subject: [PATCH 8/9] mention breeze sdk --- features/networks/spark.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/features/networks/spark.mdx b/features/networks/spark.mdx index 07f74cf6..68923ef9 100644 --- a/features/networks/spark.mdx +++ b/features/networks/spark.mdx @@ -115,6 +115,8 @@ When you deposit BTC into Spark, the deposit flow pre-signs two Bitcoin L1 trans The corollary: **these transactions must be stored durably.** If they are lost and the SOs are unavailable, your recovery path is gone. Treat them with the same care as a seed phrase — durable, encrypted, off-device storage, with backups. The Turnkey enclave does not retain them; it signs them once at deposit time and returns them to the client. +SDKs that wrap Spark model this for you: in the Breez SDK, each leaf's pre-signed exit transaction is the [`refund_tx` field on its `TreeNode`](https://github.com/breez/spark-sdk/blob/aef4a0d8939bb6ed86d9b229116afd9d450d8886/crates/spark/src/tree/mod.rs#L161), saved and reloaded through the SDK's `TreeStore`. If you build on such an SDK, configure a durable, backed-up `TreeStore` backend rather than relying on the default in-memory store. + This is the property that makes Spark Operator unavailability a _liveness_ concern rather than a _safety_ concern: you may not be able to transact, but you can always exit. ### Static deposits export a key from the enclave From 18fc4accd26baa39c2603c6483913f18e6a024d0 Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Wed, 3 Jun 2026 14:46:39 +0900 Subject: [PATCH 9/9] address feedback --- features/networks/spark.mdx | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/features/networks/spark.mdx b/features/networks/spark.mdx index 68923ef9..8eb5cfd2 100644 --- a/features/networks/spark.mdx +++ b/features/networks/spark.mdx @@ -63,17 +63,19 @@ The supported `IDENTITY` address formats are: When creating a wallet account via the Turnkey dashboard or API, select `ADDRESS_FORMAT_SPARK_MAINNET` or `ADDRESS_FORMAT_SPARK_REGTEST` and the identity path will be set automatically. Only `secp256k1` keys are supported; `ed25519` keys will be rejected. -### Schnorr signing on the identity key +### Signing configurations -Spark payloads signed with the identity key use **plain BIP-340 Schnorr**, without the Taproot key tweak that Bitcoin P2TR addresses require (see [Bitcoin Schnorr signatures and tweaks](/features/networks/bitcoin#schnorr-signatures-and-tweaks) for the contrast). Turnkey picks the right signing scheme from the address format on your key: +When you call [`SIGN_RAW_PAYLOAD`](/api-reference/activities/sign-raw-payload) with a Spark identity address as `signWith`, as in the case of signing Spark token transactions (example [here](https://github.com/tkhq/sdk/tree/main/examples/with-spark)), Turnkey produces a **plain BIP-340 Schnorr** signature — without the Taproot key tweak that Bitcoin P2TR addresses require (see [Bitcoin Schnorr signatures and tweaks](/features/networks/bitcoin#schnorr-signatures-and-tweaks) for the contrast). Generally, Turnkey will pick the scheme from the address format passed as `signWith`: -| Address type | Signing scheme | +| `signWith` address | Signing scheme | | --------------------- | ------------------------- | | Bitcoin P2TR | Tweaked Schnorr (BIP-341) | | Spark Mainnet/Regtest | Plain Schnorr (BIP-340) | | All others | ECDSA | -Use [`SIGN_RAW_PAYLOAD`](/api-reference/activities/sign-raw-payload) with the Spark address as `signWith` to produce a plain-Schnorr signature. The `hashFunction` field should match how the payload was prepared (e.g. `HASH_FUNCTION_NO_OP` for a pre-hashed payload). The returned signature always has `V = "00"` since Schnorr signatures do not carry a recovery ID. +The `hashFunction` field should match how the payload was prepared (e.g. `HASH_FUNCTION_NO_OP` for a pre-hashed payload). The returned signature always has `V = "00"` since Schnorr signatures do not carry a recovery ID. + +This scheme selection applies only to `SIGN_RAW_PAYLOAD`. The Spark-specific activities sign with whatever the protocol expects regardless of address format — for example, [`SPARK_PREPARE_TRANSFER`](/api-reference/activities/prepare-spark-transfer) signs its `transferUserSignature` with the identity key using **ECDSA** (DER-encoded), not Schnorr. This identity-key signing path is sufficient on its own for token operations via the Spark SDK (`@buildonspark/spark-sdk`, `@buildonspark/issuer-sdk`). The FROST-based flows below require the additional Spark-specific activities. @@ -109,11 +111,11 @@ Turnkey supports every Spark wallet operation. For a runnable walkthrough of eac ## Security model -### Pre-signed exit transactions are seed-phrase-equivalent +### Pre-signed exit transactions must be secured When you deposit BTC into Spark, the deposit flow pre-signs two Bitcoin L1 transactions inside the Turnkey enclave: a branch transaction and a timelocked exit transaction. These are your unilateral exit path. If every Spark Operator goes offline or acts maliciously, you can broadcast them directly to Bitcoin L1 and recover your BTC without operator cooperation. Per the [Spark sovereignty docs](https://docs.spark.money/learn/sovereignty), exiting can take "as little as 100 blocks" (~16 hours) — the actual wait depends on leaf depth and how recently the leaf was transferred, since timelocks decrement at each transfer. -The corollary: **these transactions must be stored durably.** If they are lost and the SOs are unavailable, your recovery path is gone. Treat them with the same care as a seed phrase — durable, encrypted, off-device storage, with backups. The Turnkey enclave does not retain them; it signs them once at deposit time and returns them to the client. +The corollary: **these transactions must be stored durably.** If they are lost and the SOs are unavailable, your recovery path is gone. Treat them with the same care as a seed phrase — durable, encrypted, off-device storage, with backups. Turnkey does not explicitly retain them; it signs them once at deposit time and returns them to the client. SDKs that wrap Spark model this for you: in the Breez SDK, each leaf's pre-signed exit transaction is the [`refund_tx` field on its `TreeNode`](https://github.com/breez/spark-sdk/blob/aef4a0d8939bb6ed86d9b229116afd9d450d8886/crates/spark/src/tree/mod.rs#L161), saved and reloaded through the SDK's `TreeStore`. If you build on such an SDK, configure a durable, backed-up `TreeStore` backend rather than relying on the default in-memory store. @@ -124,7 +126,11 @@ This is the property that makes Spark Operator unavailability a _liveness_ conce Static deposit addresses are reusable: one address can receive many deposits, each creating a separate Spark leaf. To make that work, the SSP needs to process deposits while your wallet is offline, which means it needs co-signing capability on the static deposit key. - Static deposits are the **only** Spark flow that takes a raw private key out of the Turnkey enclave. The flow uses [`EXPORT_WALLET_ACCOUNT`](/api-reference/activities/export-wallet-account) to export the static deposit key so it can be shared with the SSP. Every other Spark flow keeps all key material inside the enclave. + Static deposits are the **only** Spark flow that takes a raw private key out + of the Turnkey enclave. The flow uses + [`EXPORT_WALLET_ACCOUNT`](/api-reference/activities/export-wallet-account) to + export the static deposit key so it can be shared with the SSP. Every other + Spark flow keeps all key material inside the enclave. This is the intentional custodial tradeoff of static deposits — expected behavior, not a leak. The exported key controls only the **static deposit address**; it cannot move existing leaves or touch your identity key. To minimize exposure while the key is in transit: @@ -134,7 +140,11 @@ This is the intentional custodial tradeoff of static deposits — expected behav - Zero the key in your local memory immediately after transmission. - **The exported key stops mattering once you claim the deposit.** Claiming runs through [`SPARK_CLAIM_TRANSFER`](/api-reference/activities/claim-spark-transfer), which rotates the leaf to a fresh key derived inside your enclave; after that, the exported static deposit key has no authority over the funds. + **The exported key stops mattering once you claim the deposit.** Claiming runs + through + [`SPARK_CLAIM_TRANSFER`](/api-reference/activities/claim-spark-transfer), + which rotates the leaf to a fresh key derived inside your enclave; after that, + the exported static deposit key has no authority over the funds. {/* TODO: See the static deposit scripts in the [SDK example](#sdk-example) for the full claim flow. */} @@ -151,8 +161,8 @@ Every Spark flow involves direct calls from the client to the Spark Operators (f The canonical reference for integrating Turnkey with Spark is [`examples/with-spark`](https://github.com/tkhq/sdk/tree/main/examples/with-spark) in the Turnkey SDK monorepo. It contains: - A Turnkey-backed Spark signer (`TurnkeySparkSigner`) that plugs into the Spark SDK. -- End-to-end runnable flows for deposit, transfer (send + claim), withdrawal, Lightning receive and send, and static deposit. - Token-operation scripts (create, mint, transfer) using the issuer SDK. +- (Coming soon) End-to-end runnable flows for deposit, transfer (send + claim), withdrawal, Lightning receive and send, and static deposit. - Comments mapping each step to the actor model above. If you're integrating Spark, start with the SDK example and refer back to this page for the conceptual model and the security notes.