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)

Two programs, both deployed on Solana devnet:

ProgramDevnet IDPurpose
soda99apYWpnoMWwA2iXyJZcTMoTEag6tdFasjujdhdeG8b4Core primitive: init_committee, request_signature, finalize_signature
eth_demo9g9eAkNbjpkVLi692vhgcUapJKS26yQTgsLzKbXKJXWMDemo harness: builds RLP, keccaks, 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 both program IDs.
  2. Manually decodes EthTxRequested and SigCompleted borsh events.
  3. Caches (sig_request, chain_id, unsigned_rlp) from EthTxRequested.
  4. On SigCompleted, looks up the cached unsigned RLP, re-encodes with (v, r, s) via @soda-sdk/core, and POSTs eth_sendRawTransaction.

Run with pnpm relayer:dev.

TypeScript SDK (packages/soda-sdk/)

Source-only ESM workspace package. Exports derivation, RLP encode / decode, EIP-155 v calculation, and an EthRpc client. Consumed by the demo CLI, the web app, and the relayer via workspace:*.

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.