Referencesoda program

soda program

The on-chain primitive. An Anchor 0.32.1 program with three instructions and two events. Live on Solana devnet at 99apYWpnoMWwA2iXyJZcTMoTEag6tdFasjujdhdeG8b4.

Instructions

init_committee

pub fn init_committee(
    ctx: Context<InitCommittee>,
    group_pk_xy: [u8; 64],
) -> Result<()>

One-time setup. Creates the Committee PDA and stores the aggregate group public key as uncompressed X || Y. v0 uses a single dev key; v1 will add threshold + signer set.

AccountTypeNotes
committeePDA, initSeeds: [b"committee"]
payerSigner, mutPays rent
system_programProgram

request_signature

pub fn request_signature(
    ctx: Context<RequestSignature>,
    foreign_pk_xy: [u8; 64],
    seeds: Vec<u8>,
    payload: [u8; 32],
    chain_tag: u8,
) -> Result<()>

Creates a SigRequest PDA and emits SigRequested. Designed for CPI from any caller program. The caller is responsible for computing foreign_pk_xy from (group_pk, program_id, seeds, chain_tag). See Concepts → Derivation.

AccountTypeNotes
sig_requestPDA, initSeeds: [b"sig", caller_program_id, seeds_hash]
committeePDARead-only, used for group_pk_xy
payerSigner, mut
system_programProgram

finalize_signature

pub fn finalize_signature(
    ctx: Context<FinalizeSignature>,
    signature: [u8; 64],
    recovery_id: u8,
) -> Result<()>

Permissionless: anyone holding a valid signature can finalize the request. Runs secp256k1_recover(payload, recovery_id, signature) and compares the result to sig_request.foreign_pk_xy. On match, writes the signature, flips status to Completed, and emits SigCompleted.

Returns SodaError::AlreadyCompleted (0x1770) if the request is already finalized. Off-chain components treat this as success.

AccountTypeNotes
sig_requestPDA, mut
payerSigner, mut

update_committee

pub fn update_committee(
    ctx: Context<UpdateCommittee>,
    new_group_pk: [u8; 33],
    new_signer_count: u8,
) -> Result<()>

Authority-gated swap of the committee’s group_pk (and signer_count). Used to migrate from the v0 single-key signer to the v0.5 MPC committee’s joint public key without redeploying the program.

The ix is locked to committee.authority (set at init_committee time) via Anchor’s has_one = authority, so no one but the original initializer can change the group key. The on-chain layout doesn’t change — group_pk stays compressed (33 bytes), signer_count becomes whatever you pass (e.g. 2 for 2-of-2 Lindell ‘17, 3 for 2-of-3 GG18).

AccountTypeNotes
committeePDA, mutSeeds: [b"committee"], has_one = authority
authoritySignerMust match committee.authority

Used by pnpm mpc:update-committee after a fresh DKG ceremony — see Concepts → Committee.

Events

SigRequested

#[event]
pub struct SigRequested {
    pub sig_request: Pubkey,
    pub foreign_pk_xy: [u8; 64],
    pub payload: [u8; 32],
    pub chain_tag: u8,
}

Emitted by request_signature. Off-chain signers listen for this and decide whether to sign based on whether they can derive the correct tweak for foreign_pk_xy.

SigCompleted

#[event]
pub struct SigCompleted {
    pub sig_request: Pubkey,
    pub signature: [u8; 64],
    pub recovery_id: u8,
}

Emitted by finalize_signature on a successful recover-and-match. Relayers listen for this and broadcast the assembled signed transaction to the foreign chain.

Account schemas

Committee

#[account]
pub struct Committee {
    pub group_pk_xy: [u8; 64],
}

PDA seeds: [b"committee"]. One per deployment.

SigRequest

#[account]
pub struct SigRequest {
    pub foreign_pk_xy: [u8; 64],
    pub payload: [u8; 32],
    pub seeds: Vec<u8>,
    pub chain_tag: u8,
    pub signature: [u8; 64],
    pub recovery_id: u8,
    pub status: SigStatus,
}
 
pub enum SigStatus {
    Pending,
    Completed,
}

PDA seeds: [b"sig", caller_program_id, hash(seeds)]. One per (caller, seeds) pair.

Errors

CodeNameWhen
0x1770AlreadyCompletedfinalize_signature called on a Completed request
0x1771SignatureMismatchRecovered key did not equal stored foreign_pk_xy
0x1772InvalidRecoveryIdrecovery_id not in {0, 1}
0x1773InvalidChainTagchain_tag not recognized

Compute budget

finalize_signature uses around 25,000 CU for secp256k1_recover plus the usual Anchor account-deserialization overhead. The whole instruction fits comfortably in the default 200 K CU budget.

CPI usage from a caller program

use anchor_lang::prelude::*;
use soda::cpi::accounts::RequestSignature;
use soda::cpi::request_signature;
 
pub fn my_signing_call(ctx: Context<MySigningCall>) -> Result<()> {
    let payload = keccak::hash(&unsigned_rlp).0;
    let cpi_ctx = CpiContext::new(
        ctx.accounts.soda_program.to_account_info(),
        RequestSignature {
            sig_request: ctx.accounts.sig_request.to_account_info(),
            committee: ctx.accounts.committee.to_account_info(),
            payer: ctx.accounts.payer.to_account_info(),
            system_program: ctx.accounts.system_program.to_account_info(),
        },
    );
    request_signature(
        cpi_ctx,
        foreign_pk_xy,    // computed off-chain or in the caller
        seeds,            // arbitrary identifying bytes
        payload,
        chain_tag,
    )?;
    Ok(())
}

See contracts/programs/eth_demo/src/lib.rs for a complete working example that builds an Ethereum legacy RLP transaction and CPIs into soda.