Architecture

Architecture

SODA splits responsibilities across three layers: an on-chain primitive (soda Anchor program), an off-chain MPC committee (signing daemons), and a permissionless relayer that broadcasts to the foreign chain.

End-to-end pipeline

The full request-to-broadcast flow, taking the v0 ETH demo as the example:

In v0 the demo CLI also broadcasts to Sepolia in parallel with the relayer. Both attempts converge on the same transaction hash; whichever lands first wins, the other observes “already known” and exits cleanly.

Components

On-chain (Anchor)

Three programs, all deployed on Solana devnet, and demo.sh deploys any that are missing on whichever cluster you point it at:

ProgramProgram IDPurpose
soda2YDHaX2fPXdmH14hgSJQHMJQpEHrXofzhu5hDVFgFiVdCore primitive: init_committee, request_signature, finalize_signature
eth_demoGfAuUesztZ98BhUZv6ymLxvxty7matXJJ4xRh5zrvPkADemo harness: builds RLP, keccaks, CPIs into soda
sui_demo9LBE5dntoLRV61AM3W3ZHikgZPqZ5MLS4xCVvSxxbXugSui caller: derives the sender, BCS-encodes, blake2b + sha256, CPIs into soda

The soda program never imports k256 or any heavy curve crate. It uses only the Solana runtime’s secp256k1_recover syscall (~25,000 CU) for verification. That keeps the BPF stack budget intact.

Off-chain components

Signer daemon (contracts/signer/)

A Rust Tokio binary that:

  1. Subscribes to Solana logs filtered by the soda program ID.
  2. Decodes SigRequested events from Program data: <base64> log lines using borsh + the sha256("event:SigRequested")[..8] discriminator.
  3. Computes foreign_pk = group_pk + tweak·G for each known requester program and matches it against the stored foreign_pk_xy on the event. (See Concepts → Derivation.)
  4. Signs the payload with (sk + tweak) mod n via k256::ecdsa::SigningKey::sign_prehash_recoverable.
  5. Submits a finalize_signature instruction (manually built — no anchor-client dep).

Run with pnpm signer.

Relayer (apps/relayer/)

A standalone Node service that:

  1. onLogs-subscribes to the soda, eth_demo and sui_demo program IDs.
  2. Manually decodes EthTxRequested, SuiTxRequested and SigCompleted borsh events.
  3. Caches (sig_request, chain_id, unsigned_rlp) from EthTxRequested and (sig_request, sender, tx_bytes) from SuiTxRequested.
  4. On SigCompleted, the cache says which chain the signature is for: re-encode the RLP with (v, r, s) and POST eth_sendRawTransaction, or wrap 0x01 || r || s || pk and executeTransaction over Sui GraphQL.

Run with pnpm relayer:dev.

TypeScript SDK (packages/soda-sdk/)

Source-only ESM workspace package. Exports derivation, RLP encode / decode, EIP-155 v calculation, an EthRpc client, and for Sui the BCS encoders, the two-stage hash and a SuiGraphQl client. Consumed by the demo CLI, the web app, and the relayer via workspace:*.

Second chain family: Sui

Sui is the first non-EVM chain behind the primitive. It accepts secp256k1 signatures natively (scheme flag 0x01), so the committee’s ordinary ECDSA output is a valid Sui user signature once wrapped correctly.

What changed:

LayerSui-specific piece
Caller programprograms/sui_demo: sign_sui_transfer / sign_sui_tx. Derives the sender on-chain (Sui signs over it), BCS-encodes TransactionData, computes sha256(blake2b256(intent || tx)), CPIs request_signature, emits SuiTxRequested { sig_request, sender, tx_bytes }. Ships its own blake2b because Solana has no syscall for it.
Encoderpackages/soda-sdk/src/sui.ts: address blake2b256(0x01 || pk), BCS TransactionData::V1, the payload / digest hashes, the 0x01 || r || s || pk envelope, and a GraphQL client (public JSON-RPC is deprecated on Sui). Held byte-for-byte against @mysten/sui in tests.
Assembly / broadcastSuiTxRequested.tx_bytes + SigCompleted.signatureencodeSuiSignatureexecuteTransaction. apps/demo/src/demo-sui.ts does it inline; apps/relayer does it from events: it caches SuiTxRequested by sig_request, and on SigCompleted recovers the pubkey from the signature (the event does not carry it), checks blake2b256(0x01 || pk) against the cached sender, then submits over GraphQL (SUI_CHAIN picks the network). Idempotent with the CLI: a resubmission of the same bytes is logged as already submitted. The web app’s /sui page and /api/sui/finalize do the same server-side after Phantom signs.
GasThe sender’s own SUI coin objects (id, version, digest) are the gas payment and replace the nonce; the client reads them before building.
Auditapps/demo/src/verify-sui.ts (pnpm verify:sui <digest>).

What did not change: soda (no new instruction, no new field), the committee and its signing, finalize_signature and its secp256k1_recover check, SigCompleted, and the derivation formula. Only the chain tag differs, so one owner has an unrelated address on Sui and on Base. See Sign a Sui tx and the sui_demo reference.

v0.5: real Lindell ‘17 2-of-2 MPC ECDSA — live on AWS

As of 2026-05-11 the demo runs against a real threshold ECDSA committee deployed across three EC2 instances in us-east-1:

RolePublic IPHolds
mpc-node-p144.201.168.181share x1
mpc-node-p254.88.35.104share x2
mpc-coordinator32.198.7.34 (health)nothing — only forwards bytes

The on-chain Committee.group_pk was migrated from the v0 single key to the AWS joint key via the new update_committee ix. ./demo.sh now drives signing through the AWS coordinator without any flag changes. Neither node sees the joint secret. Signing runs the 4-message Lindell ‘17 protocol; on-chain secp256k1_recover verifies the result unchanged.

caller program → request_signature → SigRequested


                               apps/mpc-subscriber (Solana)
                                          │ POST /sign

                     apps/mpc-coordinator (drives 4-msg protocol)
                                  /                 \
                                 ▼                   ▼
                       mpc-node-p1            mpc-node-p2
                       (holds x1)             (holds x2)

                               r,s,v   ← combined signature


                     soda::finalize_signature  (secp256k1_recover, no change)

Run with pnpm mpc:dkg (once) → pnpm mpc:uppnpm mpc:update-committee (swap on-chain group_pk) → pnpm mpc:subscribe. See Deploy → AWS MPC for the production-shape walkthrough.

Trust model: v0 vs v0.5 vs v1

Layerv0v0.5 (today)v1 (target)
DerivationOff-chain by caller, on-chain bytes-compareSameOn-chain group_pk + tweak·G (heap-allocated / syscall / zk)
Signing keySingle dev k256 on diskLindell ‘17 2-of-2, neither node sees group_sk2-of-3+ via GG18 / GG20 / CGG21
Fault tolerance0 (one box)0 (both must be up)Tolerates n - t losses
Committee membershipHardcoded singletonHardcoded pairRestaked SOL via Solayer / Jito, slashable
Replay protectionPDA seed uniquenessSamePDA + nonce + expiry

See Concepts → Committee for the long-form v1 design.