Derivation
Every Solana PDA controls a deterministic foreign-chain address. There is no
mapping table, no registration step, and no shared state between the caller
and soda. The address is computed from the caller’s identity alone.
The formula
tweak = sha256("SODA-v1" || program_id || seeds || chain_tag)
foreign_pk = group_pk + tweak · G
foreign_addr = address_encoding(foreign_pk, chain_tag)Where:
program_idis the calling Solana program (e.g.,eth_demoor any user program).seedsare arbitrary per-request bytes (analogous to PDA seeds: a vault id, a strategy index, a user pubkey).chain_tagselects the foreign chain (ETH_SEPOLIA,ETH_MAINNET,BTC_TESTNET, etc.).group_pkis the committee’s aggregate public key on the secp256k1 curve.Gis the secp256k1 generator point.address_encodingis chain-specific: keccak256 last-20-bytes for Ethereum, HASH160 + bech32 for Bitcoin P2WPKH, and so on.
Why this works
The same tweak applied to the secret key as to the public key yields a key
pair that signs validly under the derived foreign_pk:
sk' = (group_sk + tweak) mod n
pk' = sk' · G
= (group_sk + tweak) · G
= group_sk · G + tweak · G
= group_pk + tweak · G
= foreign_pkSo a signature produced with sk' recovers to foreign_pk under the foreign
chain’s standard signature verification. From Bitcoin or Ethereum’s point of
view, a normal ECDSA signature signed a normal transaction. The chain has no
idea Solana exists.
Domain separation
The leading "SODA-v1" constant prevents tweak collisions across:
- Different versions of the protocol (v1 vs a future v2 schema).
- Other applications using the same
group_pkfor unrelated derivations.
Changing the domain string is a hard fork: every derived address changes.
v0 caveat: derivation is computed off-chain
In v0, the caller computes foreign_pk_xy and passes it into
soda::request_signature. The soda program does not verify that the
caller derived foreign_pk correctly from the formula above. It only uses
the value as the expected secp256k1_recover output during finalization.
This means a malicious caller in v0 could pass an arbitrary foreign_pk_xy
and request a signature for it. Because the committee is also a single dev
signer in v0, this does not increase real attack surface beyond “trust the
committee.” But it is not the production design.
v1 path to on-chain derivation
The first attempt did group_pk + tweak·G on-chain via k256::ProjectivePoint
ops. That blew the BPF 4 KB stack. Three viable paths to re-enable:
- Heap allocation.
Box::newthe heavy intermediates so the curve operations live on the heap (32 KB available) instead of the stack. #[inline(never)]everywhere. Spread stack frames across many small functions so no single frame exceeds the budget.- Solana point-add syscall. Lobby for / wait for a native syscall for secp256k1 point addition (analogous to the existing alt-bn128 syscalls).
- zk verification. Verify the derivation off-chain inside a SNARK and submit the proof. Alt-bn128 syscalls are already available on Solana.
See Architecture → Trust model for where this fits in the v0 → v1 progression.
Parity testing
The derivation algorithm is implemented twice for cross-language testing:
- TypeScript:
packages/soda-sdk/src/derive.ts(production path; used by the demo, web app, and relayer). - Rust:
contracts/programs/soda/src/derivation.rs(#[cfg(test)]only; kept for parity tests against the TS implementation).
Run pnpm sdk:test (TS, 13 vitest cases) and cargo test --workspace --lib
(Rust, 16 cases) to verify both implementations agree on the canonical
G + G = 2G vector and known-answer ETH address derivations.