Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Approve Messages

Pre-Alpha Disclaimer: This is a pre-alpha release for development and testing only. Signing uses a single mock signer, not real distributed MPC. All 11 protocol operations are implemented (DKG, Sign, Presign, FutureSign, ReEncryptShare, etc.) across all 4 curves and 7 signature schemes, but without real MPC security guarantees. The dWallet keys, trust model, and signing protocol are not final; do not rely on any key material until mainnet. All interfaces, APIs, and data formats are subject to change without notice. The Solana program and all on-chain data will be wiped periodically and everything will be deleted when we transition to Ika Alpha 1. This software is provided “as is” without warranty of any kind; use is entirely at your own risk and dWallet Labs assumes no liability for any damages arising from its use.

create_proposal Instruction

The create_proposal instruction creates a Proposal PDA that references a dWallet and the message to sign.

Instruction Data

OffsetFieldSize
0proposal_id32
32message_hash32
64user_pubkey32
96signature_scheme1
97quorum4
101message_approval_bump1
102bump1

Total: 103 bytes.

Accounts

#AccountWSDescription
0proposalyesnoProposal PDA (["proposal", proposal_id])
1dwalletnonodWallet account
2creatornoyesProposal creator (signer)
3payeryesyesRent payer
4system_programnonoSystem program

Implementation

#![allow(unused)]
fn main() {
fn create_proposal(
    program_id: &Address,
    accounts: &[AccountView],
    data: &[u8],
) -> ProgramResult {
    if data.len() < 103 {
        return Err(ProgramError::InvalidInstructionData);
    }
    let [proposal_account, _dwallet, creator, payer, _system_program, ..] = accounts else {
        return Err(ProgramError::NotEnoughAccountKeys);
    };

    if !creator.is_signer() {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Parse instruction data
    let proposal_id: [u8; 32] = data[0..32].try_into().unwrap();
    let message_hash: [u8; 32] = data[32..64].try_into().unwrap();
    let user_pubkey: [u8; 32] = data[64..96].try_into().unwrap();
    let signature_scheme = data[96];
    let quorum = u32::from_le_bytes(data[97..101].try_into().unwrap());
    let message_approval_bump = data[101];
    let bump = data[102];

    // Quorum must be at least 1
    if quorum == 0 {
        return Err(ProgramError::InvalidInstructionData);
    }

    // Create PDA with seeds ["proposal", proposal_id, bump]
    let bump_byte = [bump];
    let signer_seeds = [
        pinocchio::cpi::Seed::from(b"proposal" as &[u8]),
        pinocchio::cpi::Seed::from(proposal_id.as_ref()),
        pinocchio::cpi::Seed::from(bump_byte.as_ref()),
    ];
    let signer = Signer::from(&signer_seeds);

    CreateAccount {
        from: payer,
        to: proposal_account,
        lamports: minimum_balance(PROPOSAL_LEN),
        space: PROPOSAL_LEN as u64,
        owner: program_id,
    }
    .invoke_signed(&[signer])?;

    // Write all proposal fields into the account data
    let prop_data = unsafe { proposal_account.borrow_unchecked_mut() };
    prop_data[0] = PROPOSAL_DISCRIMINATOR;
    prop_data[1] = 1; // version
    // ... copy proposal_id, dwallet, message_hash, etc. ...

    Ok(())
}
}

The key points:

  • The proposal stores the message_hash and message_approval_bump so the CPI call can construct the correct MessageApproval PDA later
  • The user_pubkey and signature_scheme are passed through to approve_message when quorum is reached
  • Quorum of zero is rejected

MessageApproval PDA

When quorum is reached, the program creates a MessageApproval PDA via CPI. This PDA is derived by the dWallet program, not the voting program:

Seeds: ["message_approval", dwallet_pubkey, message_hash]
Program: DWALLET_PROGRAM_ID

Important: The message_hash you pass to approve_message is the uniqueness key for the MessageApproval PDA and must be computed as keccak256(preimage) regardless of which destination chain the dWallet will eventually sign for. The dwallet program treats it as opaque 32 bytes; using the same hash function (keccak256, with Solana’s cheap on-chain syscall) for every chain keeps the dwallet program chain-agnostic.

The digest the dwallet network actually signs is a separate concern, controlled by the hash_scheme field on the gRPC Sign request. For Secp256k1 the network applies hash_scheme(message) and signs the resulting 32-byte digest via sign_prehash, so the produced signature is valid on whichever chain expects that exact hash function (Keccak256 for EVM, DoubleSHA256 for Bitcoin BIP143, etc.). For EVM the on-chain lookup hash and the signing digest happen to coincide; for Bitcoin they differ. The mock supports hash_scheme (it used to ignore it and always hash with SHA-256, which produced signatures that wouldn’t verify on real EVM/Bitcoin nodes).

The dWallet program verifies:

  1. The caller is a valid program (executable account)
  2. The CPI authority PDA matches the dWallet’s current authority
  3. The CPI authority is signed (via invoke_signed)

Next Step

With proposals created, the next chapter implements vote casting and the quorum-triggered CPI.