Message Approval
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.
Overview
Message approval is the core mechanism for requesting signatures from the Ika network. When you call approve_message, it creates a MessageApproval PDA on-chain. The network detects this account and produces a signature.
MessageApproval Account
MessageApproval PDA:
Seeds: ["message_approval", dwallet_pubkey, message_hash]
Program: DWALLET_PROGRAM_ID
Total: 287 bytes (2 + 285)
The message_hash must be the keccak256 hash of the message you want signed:
#![allow(unused)]
fn main() {
let message_hash = solana_sdk::keccak::hash(message).to_bytes();
}
import { keccak_256 } from "@noble/hashes/sha3.js";
const messageHash = keccak_256(message);
This is consistent across all examples, the mock, and the gRPC service. Using any other hash function will result in a PDA mismatch when the network tries to commit the signature on-chain.
| Offset | Field | Size | Description |
|---|---|---|---|
| 0 | discriminator | 1 | 14 |
| 1 | version | 1 | 1 |
| 2 | dwallet | 32 | dWallet account pubkey |
| 34 | message_hash | 32 | Hash of the message to sign |
| 66 | user_pubkey | 32 | User public key |
| 98 | signature_scheme | 1 | Ed25519(0), Secp256k1(1), Secp256r1(2) |
| 99 | caller_program | 32 | Program that created this approval |
| 131 | cpi_authority | 32 | CPI authority PDA that signed |
| 139 | status | 1 | Pending(0) or Signed(1) |
| 140 | signature_len | 2 | Length of the signature (LE u16) |
| 142 | signature | 128 | Signature bytes (padded) |
Approval Flow
Direct Approval (User Signer)
When the dWallet’s authority is a user wallet:
User signs approve_message instruction
→ dWallet program verifies user == dwallet.authority
→ Creates MessageApproval PDA (status = Pending)
CPI Approval (Program Signer)
When the dWallet’s authority is a CPI authority PDA:
Your program calls DWalletContext::approve_message
→ invoke_signed with CPI authority seeds
→ dWallet program verifies:
- caller_program is executable
- cpi_authority == PDA(["__ika_cpi_authority"], caller_program)
- dwallet.authority == cpi_authority
→ Creates MessageApproval PDA (status = Pending)
approve_message Instruction
Discriminator: 8
Instruction Data (67 bytes):
| Offset | Field | Size |
|---|---|---|
| 0 | discriminator | 1 |
| 1 | bump | 1 |
| 2 | message_hash | 32 |
| 34 | user_pubkey | 32 |
| 66 | signature_scheme | 1 |
Accounts (CPI path):
| # | Account | W | S | Description |
|---|---|---|---|---|
| 0 | message_approval | yes | no | MessageApproval PDA (must be empty) |
| 1 | dwallet | no | no | dWallet account |
| 2 | caller_program | no | no | Calling program (executable) |
| 3 | cpi_authority | no | yes | CPI authority PDA (signed via invoke_signed) |
| 4 | payer | yes | yes | Rent payer |
| 5 | system_program | no | no | System program |
Signature Lifecycle
- Pending: Your program calls
approve_message→ MessageApproval created,status = 0,signature_len = 0 - gRPC Sign: You send a
Signrequest via gRPC withApprovalProofreferencing the on-chain approval. The network returns the 64-byte signature directly and commits it on-chain viaCommitSignature. - Signed:
status = 1, signature bytes written at offset 142, readable by anyone.
Your program calls approve_message (CPI)
→ MessageApproval PDA created (status = Pending)
→ You send gRPC Sign request with ApprovalProof
→ Network signs and returns signature via gRPC
→ Network calls CommitSignature on-chain
→ status = Signed, signature available at offset 142
The signature is available both from the gRPC response and on-chain in the MessageApproval account.
CommitSignature Instruction
Called by the NOA to write the signature into the MessageApproval account.
Discriminator: 43
Instruction Data:
| Offset | Field | Size |
|---|---|---|
| 0 | discriminator | 1 |
| 1 | signature_len | 2 |
| 3 | signature | 128 |
Accounts:
| # | Account | W | S | Description |
|---|---|---|---|---|
| 0 | message_approval | yes | no | MessageApproval PDA |
| 1 | nek | no | no | NetworkEncryptionKey PDA |
| 2 | noa | no | yes | NOA signer |
Reading the Signature
#![allow(unused)]
fn main() {
let data = client.get_account(&message_approval_pda)?.data;
let status = data[139];
if status == 1 {
let sig_len = u16::from_le_bytes(data[140..142].try_into().unwrap()) as usize;
let signature = &data[142..142 + sig_len];
// Use the signature
}
}
Idempotency
The same (dwallet, message_hash) pair always derives the same MessageApproval PDA. Attempting to create a MessageApproval that already exists will fail (the account is non-empty). This prevents duplicate signing requests.