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 an early pre-alpha release for exploring the SDK and starting development only. There is no real MPC signing — all signatures are generated by a single mock signer, not a distributed network. Do not submit any real transactions for signing or rely on any 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 must be computed using keccak256. This is required for the network (or mock) to derive the same PDA when committing the signature on-chain. This convention is subject to change in future releases — the pre-alpha mock always uses keccak256 regardless of the hash_scheme field in the gRPC Sign request.

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.