Components
The intent path
- Client signs. The SDK builds a canonical intent message (chain id, transfers, action, expiry), SHA-256 hashes it, and signs with the identity key. See Intents.
- Gateway admits. A REST gateway authenticates the signer, validates the expiry, checks the intent isn’t a replay, and forwards to the sequencer.
- Sequencer admission and deposit oracle. The sequencer holds deposit-shaped intents in
ACCEPTEDwhile the deposit oracle confirms each Spark transfer id against the Spark operator DB. Intents without deposits skip this wait. A failure here or an expiry transitions the intent toEXPIRED, with the reason instatusMessage. - Sequencer includes. Once admitted, the sequencer orders the intent into the next block as one or more EIP-1559 transactions: deposit calls to
SparkGatewayplus the user’s signed transaction. - Validators finalize. Validators run the block, compare state roots, and sign a finality certificate inside their TEE. Quorum advances the chain head.
- Settlement dispatches. Finalized
SparkWithdrawalevents are translated into Spark transfers and dispatched back to the user’s Spark wallet.
Why TEEs
All consensus signing and Spark wallet operations run inside a TEE signing server (tee-server). Validator and sequencer host processes talk to it over VSock and never hold keys themselves. In production the TEE is an AWS Nitro Enclave, with the signing key sealed via KMS and unsealed only on PCR-attested startup. Two things this buys:
- Key isolation. Finality-certificate signing keys, deposit-oracle signing keys, and Spark wallet signing keys all live inside the enclave. A compromised validator or sequencer host can’t forge a signature.
- Attestable code. The enclave produces a remote attestation that proves the running binary matches a known measurement. Operators publish the expected measurement; clients can verify the validator they’re talking to is running the code they expect.
Why intents instead of transactions
Three things plain transactions can’t express:| Need | Why intents |
|---|---|
| Atomicity across Spark and EVM | An intent commits Spark deposits and an EVM action together. Either both apply or neither does. |
| Settlement back to Spark | The settlement layer reads finalized intents and translates SparkWithdrawal events into Spark transfers. The intent boundary is its input. |
| Replay protection across both sides | The intent’s nonce binds the deposit ids, the EVM transaction, and the expiry into one signed preimage. A replay can’t reuse a deposit, a tx, or a recipient in isolation. |
Why zero gas
The runtime setsbaseFeePerGas = 0 and maxPriorityFeePerGas = 0 for every block. There is no base fee to charge, no priority fee to bid, and no gas-token economy to manage.
This means:
- No gas token. Native sats are usable, but you don’t need to hold them just to call a contract.
- No fee market. Transactions are ordered by sequencer policy, not by bid.
- The fee policy is settable. The integration is forward-compatible:
fetchEip1559Fees(rpcUrl)returns whatever the node currently reports, so a future fee change doesn’t break clients that read it.
Replay safety
Two layers of replay protection:- Spark transfer ids are recorded on
SparkGatewayafter first use. A second intent claiming the same transfer id is rejected at admission. - Withdrawal nonces are monotonic on
SparkGateway. The settlement layer reconciles by nonce, so a finalized withdrawal never settles twice on Spark even if the EVM transaction is replayed.
What’s not on chain
- The signed intent message. It’s the input the gateway and validators verify against. Only the resulting EVM transactions land on the chain.
- Spark transfer ids in their original form. They’re embedded in the deposit call’s calldata as 32-byte values for replay protection but don’t appear as chain events outside that.
- Spark addresses. Withdrawals carry the 33-byte compressed pubkey of the recipient in calldata. The chain doesn’t know it’s a Spark address; the settlement layer does.