This page documents each Spark flow in detail, showing exactly which Turnkey API activities are called at each step and what happens inside the enclave. All private key material and secret shares remain inside the Turnkey enclave throughout — they never reach client code. For quick-reference code examples, see the Spark network overview.Documentation Index
Fetch the complete documentation index at: https://turnkey-0e7c1f5b-omkar-spark-phase2-docs.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Understanding Spark Operators (SOs)
Spark leaves are jointly controlled by your identity key and the Spark Operator collective (the SE), using FROST threshold signing. SOs never hold your complete key — they hold threshold shares — and the protocol is designed around a 1-of-n trust assumption: as long as one operator is honest, funds cannot be stolen. The worst case if all operators are unavailable is that you cannot transact, not that funds are lost. The current operators are Lightspark and Flashnet. For definitions of SE, SO, and SSP (Spark Service Provider), see Spark core concepts. For a deeper protocol walkthrough, see the Spark technical overview. SO communication happens outside Turnkey. Steps labeled SO call or SSP call throughout this page are direct client-to-operator API calls — Turnkey is not involved in those steps. Turnkey only handles the enclave key operations (SPARK_SIGN_FROST, SPARK_PREPARE_TRANSFER, etc.). For a working example of how to wire the Turnkey signer into the Spark SDK, refer to the JS code example provided with your Spark integration.
1. Deposit: Bitcoin L1 → Spark
Turnkey activities:CREATE_WALLET_ACCOUNTS ×1, SPARK_SIGN_FROST ×1 (batch of 2)
Step 1 — Derive deposit key
CallCREATE_WALLET_ACCOUNTS to derive and register a single-use deposit key in the enclave:
Step 2 — Get SO public key shares
SO call: Request SO public key shares for this deposit leaf. The SOs respond with their key share, which you combine with your deposit public key to compute the aggregate Taproot address. See Deposits from L1.
Step 3 — Compute aggregate public key and Taproot deposit address
Client-side:P_aggregate = P_user_deposit + P_so. Derive the bc1p... Taproot address.
Step 4 — Construct branch and exit transactions
Client-side: build unsignedTxn1 (branch) and Txn2 (exit, timelocked) using the aggregate public key.
Step 5 — Get SO signing commitments
SO call: Request FROST nonce commitments for the branch and exit transaction signing sessions. The SOs return two sets of operatorCommitments[]. See Deposits from L1.
Step 6 — FROST sign branch and exit transactions
CallSPARK_SIGN_FROST with a batch of 2 signatures:
(signatureShare, hiding, binding) tuples. The client aggregates each with the SOs’ partial signatures to produce 2 final Schnorr signatures. Both transactions are now fully signed.
Step 7 — Store signed exit transactions
StoreTxn1 and Txn2 securely. These are the permanent unilateral exit path — see Unilateral Exit.
Step 8 — Broadcast deposit transaction
Broadcast a standard Bitcoin transaction sending BTC to thebc1p... Taproot address.
Step 9 — Wait for L1 confirmation
The SOs register the new Spark leaf once the transaction confirms.2. Cooperative Withdrawal: Spark → Bitcoin L1
Turnkey activities:SPARK_SIGN_FROST ×1
Step 1 — Construct withdrawal transaction
Client-side: build an unsigned Bitcoin L1 transaction spending from the leaf’s Taproot address to the user’s personal Bitcoin address.Step 2 — Get SO signing commitments
SO call: Request FROST nonce commitments for the cooperative withdrawal signing session. The SOs return operatorCommitments[]. See Withdrawals.
Step 3 — FROST sign withdrawal transaction
CallSPARK_SIGN_FROST:
(signatureShare, hiding, binding). The client forwards the nonces to the SOs and aggregates all partial signatures into the final Schnorr signature.
Step 4 — Broadcast withdrawal transaction
Attach the final signature and broadcast to Bitcoin L1.Step 5 — Wait for confirmation
BTC arrives at the user’s L1 address. The SOs deactivate the leaf.3. Unilateral Exit: Spark → Bitcoin L1 (Emergency)
Turnkey activities: None This path uses transactions that were pre-signed at deposit time. No Turnkey API calls are required.Step 1 — Retrieve pre-signed exit transactions
Retrieve the storedTxn1 (branch) and Txn2 (exit) from deposit time.
Step 2 — Broadcast branch transaction
BroadcastTxn1 to Bitcoin L1. Once mined, the relative timelock countdown begins.
Step 3 — Wait for timelock
Wait approximately 100 blocks (~16 hours).Step 4 — Broadcast exit transaction
BroadcastTxn2. BTC arrives at the user’s L1 address.
The unilateral exit path requires no Turnkey API calls and no SO cooperation — exit transactions were fully signed at deposit time using
SPARK_SIGN_FROST and can be broadcast directly to Bitcoin L1. This is the property that makes SO unavailability a liveness concern rather than a safety concern. See Trust model.4. Initiate Transfer: Spark → Spark (Sender)
Turnkey activities:SPARK_SIGN_FROST ×1 (batch of up to 6), SPARK_PREPARE_TRANSFER ×1
Step 1 — Select input leaves
SO call: Query the SOs for the sender’s active leaf set to determine which leaves cover the transfer amount. See Transfers.
Step 2 — Get SO signing commitments for all new exit transactions
SO call: Request FROST nonce commitments for refund transactions across all new leaves. The SOs return operatorCommitments[] for each of 3 refund transaction types per new leaf (CPFP, direct, direct-from-CPFP). For 2 new leaves (recipient leaf + change leaf) this yields 6 sets of commitments. See FROST signing.
Step 3 — FROST sign all refund transactions
CallSPARK_SIGN_FROST with a batch of up to 6 signatures (3 types × up to 2 leaves):
(signatureShare, hiding, binding) tuples. The client aggregates each with the SOs’ partial signatures to produce 6 final Schnorr signatures. All new leaf exit transactions are now pre-signed.
Step 4 — Build the transfer package
CallSPARK_PREPARE_TRANSFER with the aggregated refund signatures from Step 3:
- Derives old and new leaf keys from the identity key’s HD wallet
- Computes
tweak = oldKey − newKeyper leaf - Feldman-splits each tweak for the SOs
- ECIES-encrypts Bob’s new leaf key to his identity public key
- ECIES-encrypts each SO’s Shamir share to their encryption public key
- ECDSA-signs the per-leaf payload and the outer transfer payload with the identity key
operatorPackages[] and transferUserSignature.
Step 5 — Submit to Spark Operators
SO call: Send the aggregated refund signatures from Step 3, the encrypted operator packages from Step 4, and the transferUserSignature from Step 4 to each SO. The SOs verify, rotate key shares, delete old shares, store the ECIES blob for Bob, register new leaves, and activate watchtowers. See Transfers and Trust model.
5. Claim Transfer: Spark → Spark (Receiver)
Turnkey activities:SPARK_CLAIM_TRANSFER ×1
Step 1 — Poll SOs for pending transfers
SO call: Query the SOs for pending inbound transfers. The SOs return the encrypted blob, leaf ID, transfer ID, and sender identity public key for each pending transfer. See Transfers.
Step 2 — Claim the transfer
CallSPARK_CLAIM_TRANSFER:
- Verifies Alice’s ECDSA signature over
SHA256(leafId || transferId || ciphertext) - ECIES-decrypts the ciphertext using Bob’s identity private key to recover
current_leaf_priv - Derives Bob’s new leaf key from his HD wallet
- Computes
claim_tweak = current_leaf_priv − new_leaf_priv - Feldman-splits the claim tweak for the SOs
- ECIES-encrypts each SO’s claim share to their encryption public key
operatorPackages[].
Step 3 — Submit claim packages to SOs
SO call: Send the encrypted claim packages to each SO. Each SO applies the tweak, rotating the leaf key to Bob’s HD-derived key. See Transfers.
Step 4 — Store new leaf in local wallet state
Record theleafId → derivation path mapping locally. Bob’s wallet now tracks this leaf.
6. Lightning Receive
Turnkey activities:SPARK_PREPARE_LIGHTNING_RECEIVE ×1
Step 1 — Generate preimage and distribute shares to SOs
CallSPARK_PREPARE_LIGHTNING_RECEIVE:
- Generates a random 32-byte preimage using a secure RNG
- Computes
paymentHash = SHA256(preimage) - Feldman-splits the preimage across the SOs
- ECIES-encrypts each SO’s share to their encryption public key
operatorPackages[] and paymentHash. The raw preimage never leaves the enclave.
Step 2 — Submit preimage shares to SOs
SO call: Send the encrypted operator packages to each SO. Each SO stores its share and will contribute to preimage reconstruction when the Lightning payment arrives.
Step 3 — Create Lightning invoice
Construct a BOLT11 invoice encodingpaymentHash. Share this invoice with the payer.
Step 4 — Lock leaves to the SSP
SSP call: Notify the Spark Service Provider that you will transfer X sats of your leaves in exchange for payment of the invoice. The SOs lock the specified leaves. See Spark core concepts for the SSP role.
Step 5 — SSP pays the Lightning invoice
SSP call: The SSP routes the payment across the Lightning Network.
Step 6 — SOs reveal preimage to SSP
SO call: Once the incoming Lightning payment is detected, threshold SOs reconstruct the preimage from their Feldman shares and reveal it to the SSP.
Step 7 — SSP completes the Lightning payment
SSP call: The SSP reveals the preimage to the payer’s node. The Lightning payment settles.
Step 8 — SOs transfer leaves to the SSP
SO call: The SOs atomically execute the leaf transfer from the receiver to the SSP.
Timeout path
If the SSP doesn’t pay within the time limit, the SOs unlock the receiver’s leaves. No funds are lost.7. Static Deposit: Bitcoin L1 → Spark (Reusable Address)
Turnkey activities:CREATE_WALLET_ACCOUNTS ×1 (once per index, reused thereafter), SPARK_SIGN_FROST ×1 (batch of 2)
A static deposit address is deterministic and permanent — the same path at index N always produces the same Taproot address and can receive multiple deposits, each creating a separate Spark leaf.
Step 1 — Derive static deposit key
CallCREATE_WALLET_ACCOUNTS once per index. Increment the final path segment for additional static addresses:
Step 2 — Get SO public key shares
SO call: Request SO public key shares for this static deposit index. The SOs respond with their key share, which you combine with your static deposit public key to compute the aggregate Taproot address. See Deposits from L1.
Step 3 — Compute aggregate public key and Taproot deposit address
Client-side:P_aggregate = P_static_deposit_user + P_so. Derive the permanent bc1p... Taproot address for this index.
Step 4 — Construct branch and exit transactions
Client-side: build unsignedTxn1 (branch) and Txn2 (exit, timelocked) using the aggregate public key.
Step 5 — Get SO signing commitments
SO call: Request FROST nonce commitments for the branch and exit transaction signing sessions at this static deposit index. The SOs return two sets of operatorCommitments[]. See Deposits from L1.
Step 6 — FROST sign branch and exit transactions
CallSPARK_SIGN_FROST using SPARK_KEY_TYPE_STATIC_DEPOSIT_HD and the index:
(signatureShare, hiding, binding) tuples. The client aggregates with SO partial signatures to produce 2 final Schnorr signatures.
Step 7 — Store signed exit transactions
StoreTxn1 and Txn2. Each individual deposit to this address generates a separate leaf with its own exit transactions.
Step 8 — Broadcast deposit transaction
Broadcast a standard Bitcoin transaction sending BTC to the staticbc1p... Taproot address.
Step 9 — Wait for L1 confirmation
The SOs register a new Spark leaf for each confirmed deposit to this address.8. Static Deposit Claim
Turnkey activities:EXPORT_WALLET_ACCOUNT ×1
The Spark SDK’s static deposit claim path requires the raw private key to sign the claim transaction directly. Turnkey exports the key into a P256-encrypted bundle that only your local session can decrypt. The key is held in memory only for the duration of the claim.
Step 1 — Confirmed deposit detected
After a deposit to the static address confirms on-chain, the SOs register a new leaf.
SSP call: Fetch a fee quote for the claim: wallet.getClaimStaticDepositQuote(txid, outputIndex).
Step 2 — Export static deposit key
CallEXPORT_WALLET_ACCOUNT to retrieve the private key in a P256-encrypted bundle that only the local session can decrypt:
Step 3 — Claim the static deposit
- Install the decrypted private key:
signer.setStaticDepositSecretKey(0, key) - Call
wallet.claimStaticDeposit({ transactionId, creditAmountSats, sspSignature }) - Immediately zero the key:
signer.clearStaticDepositSecretKey(0)
Step 4 — Wait for balance availability
Pollwallet.getBalance() until the credited sats appear as available.