//! A library for generating a message from a sequence of instructions use crate::sanitize::{Sanitize, SanitizeError}; use crate::{ hash::Hash, instruction::{AccountMeta, CompiledInstruction, Instruction}, pubkey::Pubkey, short_vec, system_instruction, }; use itertools::Itertools; use std::convert::TryFrom; fn position(keys: &[Pubkey], key: &Pubkey) -> u8 { keys.iter().position(|k| k == key).unwrap() as u8 } fn compile_instruction(ix: &Instruction, keys: &[Pubkey]) -> CompiledInstruction { let accounts: Vec<_> = ix .accounts .iter() .map(|account_meta| position(keys, &account_meta.pubkey)) .collect(); CompiledInstruction { program_id_index: position(keys, &ix.program_id), data: ix.data.clone(), accounts, } } fn compile_instructions(ixs: &[Instruction], keys: &[Pubkey]) -> Vec { ixs.iter().map(|ix| compile_instruction(ix, keys)).collect() } /// A helper struct to collect pubkeys referenced by a set of instructions and read-only counts #[derive(Debug, PartialEq, Eq)] struct InstructionKeys { pub signed_keys: Vec, pub unsigned_keys: Vec, pub num_readonly_signed_accounts: u8, pub num_readonly_unsigned_accounts: u8, } impl InstructionKeys { fn new( signed_keys: Vec, unsigned_keys: Vec, num_readonly_signed_accounts: u8, num_readonly_unsigned_accounts: u8, ) -> Self { Self { signed_keys, unsigned_keys, num_readonly_signed_accounts, num_readonly_unsigned_accounts, } } } /// Return the pubkey of the first writable signer in the given set of instructions. fn find_writable_signer(instructions: &[Instruction]) -> Option<&Pubkey> { for instruction in instructions { for account in &instruction.accounts { if account.is_signer && account.is_writable { return Some(&account.pubkey); } } } None } /// Return pubkeys referenced by all instructions, with the ones needing signatures first. If the /// payer key is provided, it is always placed first in the list of signed keys. Read-only signed /// accounts are placed last in the set of signed accounts. Read-only unsigned accounts, /// including program ids, are placed last in the set. No duplicates and order is preserved. fn get_keys(instructions: &[Instruction], payer: Option<&Pubkey>) -> InstructionKeys { let programs: Vec<_> = get_program_ids(instructions) .iter() .map(|program_id| AccountMeta { pubkey: *program_id, is_signer: false, is_writable: false, }) .collect(); let mut keys_and_signed: Vec<_> = instructions .iter() .flat_map(|ix| ix.accounts.iter()) .collect(); keys_and_signed.extend(&programs); keys_and_signed.sort_by(|x, y| { y.is_signer .cmp(&x.is_signer) .then(y.is_writable.cmp(&x.is_writable)) }); let payer_account_meta; if let Some(payer) = payer { payer_account_meta = AccountMeta { pubkey: *payer, is_signer: true, is_writable: true, }; keys_and_signed.insert(0, &payer_account_meta); } let mut unique_metas: Vec = vec![]; for account_meta in keys_and_signed { // Promote to writable if a later AccountMeta requires it if let Some(x) = unique_metas .iter_mut() .find(|x| x.pubkey == account_meta.pubkey) { x.is_writable |= account_meta.is_writable; continue; } unique_metas.push(account_meta.clone()); } let mut signed_keys = vec![]; let mut unsigned_keys = vec![]; let mut num_readonly_signed_accounts = 0; let mut num_readonly_unsigned_accounts = 0; for account_meta in unique_metas { if account_meta.is_signer { signed_keys.push(account_meta.pubkey); if !account_meta.is_writable { num_readonly_signed_accounts += 1; } } else { unsigned_keys.push(account_meta.pubkey); if !account_meta.is_writable { num_readonly_unsigned_accounts += 1; } } } InstructionKeys::new( signed_keys, unsigned_keys, num_readonly_signed_accounts, num_readonly_unsigned_accounts, ) } /// Return program ids referenced by all instructions. No duplicates and order is preserved. fn get_program_ids(instructions: &[Instruction]) -> Vec { instructions .iter() .map(|ix| ix.program_id) .unique() .collect() } #[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "camelCase")] pub struct MessageHeader { /// The number of signatures required for this message to be considered valid. The /// signatures must match the first `num_required_signatures` of `account_keys`. /// NOTE: Serialization-related changes must be paired with the direct read at sigverify. pub num_required_signatures: u8, /// The last num_readonly_signed_accounts of the signed keys are read-only accounts. Programs /// may process multiple transactions that load read-only accounts within a single PoH entry, /// but are not permitted to credit or debit lamports or modify account data. Transactions /// targeting the same read-write account are evaluated sequentially. pub num_readonly_signed_accounts: u8, /// The last num_readonly_unsigned_accounts of the unsigned keys are read-only accounts. pub num_readonly_unsigned_accounts: u8, } #[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "camelCase")] pub struct Message { /// The message header, identifying signed and read-only `account_keys` /// NOTE: Serialization-related changes must be paired with the direct read at sigverify. pub header: MessageHeader, /// All the account keys used by this transaction #[serde(with = "short_vec")] pub account_keys: Vec, /// The id of a recent ledger entry. pub recent_blockhash: Hash, /// Programs that will be executed in sequence and committed in one atomic transaction if all /// succeed. #[serde(with = "short_vec")] pub instructions: Vec, } impl Sanitize for Message { fn sanitize(&self) -> std::result::Result<(), SanitizeError> { // signing area and read-only non-signing area should not overlap if self.header.num_required_signatures as usize + self.header.num_readonly_unsigned_accounts as usize > self.account_keys.len() { return Err(SanitizeError::IndexOutOfBounds); } // there should be at least 1 RW fee-payer account. if self.header.num_readonly_signed_accounts >= self.header.num_required_signatures { return Err(SanitizeError::IndexOutOfBounds); } for ci in &self.instructions { if ci.program_id_index as usize >= self.account_keys.len() { return Err(SanitizeError::IndexOutOfBounds); } // A program cannot be a payer. if ci.program_id_index == 0 { return Err(SanitizeError::IndexOutOfBounds); } for ai in &ci.accounts { if *ai as usize >= self.account_keys.len() { return Err(SanitizeError::IndexOutOfBounds); } } } self.account_keys.sanitize()?; self.recent_blockhash.sanitize()?; self.instructions.sanitize()?; Ok(()) } } impl Message { pub fn new_with_compiled_instructions( num_required_signatures: u8, num_readonly_signed_accounts: u8, num_readonly_unsigned_accounts: u8, account_keys: Vec, recent_blockhash: Hash, instructions: Vec, ) -> Self { Self { header: MessageHeader { num_required_signatures, num_readonly_signed_accounts, num_readonly_unsigned_accounts, }, account_keys, recent_blockhash, instructions, } } pub fn new(instructions: &[Instruction]) -> Self { let payer = find_writable_signer(instructions).expect("no suitable key for fee-payer"); Self::new_with_payer(instructions, Some(payer)) } pub fn new_with_payer(instructions: &[Instruction], payer: Option<&Pubkey>) -> Self { let InstructionKeys { mut signed_keys, unsigned_keys, num_readonly_signed_accounts, num_readonly_unsigned_accounts, } = get_keys(instructions, payer); let num_required_signatures = signed_keys.len() as u8; signed_keys.extend(&unsigned_keys); let instructions = compile_instructions(instructions, &signed_keys); Self::new_with_compiled_instructions( num_required_signatures, num_readonly_signed_accounts, num_readonly_unsigned_accounts, signed_keys, Hash::default(), instructions, ) } pub fn new_with_nonce( mut instructions: Vec, payer: Option<&Pubkey>, nonce_account_pubkey: &Pubkey, nonce_authority_pubkey: &Pubkey, ) -> Self { let nonce_ix = system_instruction::advance_nonce_account( &nonce_account_pubkey, &nonce_authority_pubkey, ); instructions.insert(0, nonce_ix); Self::new_with_payer(&instructions, payer) } pub fn serialize(&self) -> Vec { bincode::serialize(self).unwrap() } pub fn program_ids(&self) -> Vec<&Pubkey> { self.instructions .iter() .map(|ix| &self.account_keys[ix.program_id_index as usize]) .collect() } pub fn is_key_passed_to_program(&self, index: usize) -> bool { if let Ok(index) = u8::try_from(index) { for ix in self.instructions.iter() { if ix.accounts.contains(&index) { return true; } } } false } pub fn program_position(&self, index: usize) -> Option { let program_ids = self.program_ids(); program_ids .iter() .position(|&&pubkey| pubkey == self.account_keys[index]) } pub fn is_writable(&self, i: usize) -> bool { i < (self.header.num_required_signatures - self.header.num_readonly_signed_accounts) as usize || (i >= self.header.num_required_signatures as usize && i < self.account_keys.len() - self.header.num_readonly_unsigned_accounts as usize) } pub fn is_signer(&self, i: usize) -> bool { i < self.header.num_required_signatures as usize } pub fn get_account_keys_by_lock_type(&self) -> (Vec<&Pubkey>, Vec<&Pubkey>) { let mut writable_keys = vec![]; let mut readonly_keys = vec![]; for (i, key) in self.account_keys.iter().enumerate() { if self.is_writable(i) { writable_keys.push(key); } else { readonly_keys.push(key); } } (writable_keys, readonly_keys) } } #[cfg(test)] mod tests { use super::*; use crate::{ instruction::AccountMeta, signature::{Keypair, Signer}, }; #[test] fn test_message_unique_program_ids() { let program_id0 = Pubkey::default(); let program_ids = get_program_ids(&[ Instruction::new(program_id0, &0, vec![]), Instruction::new(program_id0, &0, vec![]), ]); assert_eq!(program_ids, vec![program_id0]); } #[test] fn test_message_unique_program_ids_not_adjacent() { let program_id0 = Pubkey::default(); let program_id1 = Pubkey::new_rand(); let program_ids = get_program_ids(&[ Instruction::new(program_id0, &0, vec![]), Instruction::new(program_id1, &0, vec![]), Instruction::new(program_id0, &0, vec![]), ]); assert_eq!(program_ids, vec![program_id0, program_id1]); } #[test] fn test_message_unique_program_ids_order_preserved() { let program_id0 = Pubkey::new_rand(); let program_id1 = Pubkey::default(); // Key less than program_id0 let program_ids = get_program_ids(&[ Instruction::new(program_id0, &0, vec![]), Instruction::new(program_id1, &0, vec![]), Instruction::new(program_id0, &0, vec![]), ]); assert_eq!(program_ids, vec![program_id0, program_id1]); } #[test] fn test_message_unique_keys_both_signed() { let program_id = Pubkey::default(); let id0 = Pubkey::default(); let keys = get_keys( &[ Instruction::new(program_id, &0, vec![AccountMeta::new(id0, true)]), Instruction::new(program_id, &0, vec![AccountMeta::new(id0, true)]), ], None, ); assert_eq!(keys, InstructionKeys::new(vec![id0], vec![], 0, 0)); } #[test] fn test_message_unique_keys_signed_and_payer() { let program_id = Pubkey::default(); let id0 = Pubkey::default(); let keys = get_keys( &[Instruction::new( program_id, &0, vec![AccountMeta::new(id0, true)], )], Some(&id0), ); assert_eq!(keys, InstructionKeys::new(vec![id0], vec![], 0, 0)); } #[test] fn test_message_unique_keys_unsigned_and_payer() { let program_id = Pubkey::default(); let id0 = Pubkey::default(); let keys = get_keys( &[Instruction::new( program_id, &0, vec![AccountMeta::new(id0, false)], )], Some(&id0), ); assert_eq!(keys, InstructionKeys::new(vec![id0], vec![], 0, 0)); } #[test] fn test_message_unique_keys_one_signed() { let program_id = Pubkey::default(); let id0 = Pubkey::default(); let keys = get_keys( &[ Instruction::new(program_id, &0, vec![AccountMeta::new(id0, false)]), Instruction::new(program_id, &0, vec![AccountMeta::new(id0, true)]), ], None, ); assert_eq!(keys, InstructionKeys::new(vec![id0], vec![], 0, 0)); } #[test] fn test_message_unique_keys_one_readonly_signed() { let program_id = Pubkey::default(); let id0 = Pubkey::default(); let keys = get_keys( &[ Instruction::new(program_id, &0, vec![AccountMeta::new_readonly(id0, true)]), Instruction::new(program_id, &0, vec![AccountMeta::new(id0, true)]), ], None, ); // Ensure the key is no longer readonly assert_eq!(keys, InstructionKeys::new(vec![id0], vec![], 0, 0)); } #[test] fn test_message_unique_keys_one_readonly_unsigned() { let program_id = Pubkey::default(); let id0 = Pubkey::default(); let keys = get_keys( &[ Instruction::new(program_id, &0, vec![AccountMeta::new_readonly(id0, false)]), Instruction::new(program_id, &0, vec![AccountMeta::new(id0, false)]), ], None, ); // Ensure the key is no longer readonly assert_eq!(keys, InstructionKeys::new(vec![], vec![id0], 0, 0)); } #[test] fn test_message_unique_keys_order_preserved() { let program_id = Pubkey::default(); let id0 = Pubkey::new_rand(); let id1 = Pubkey::default(); // Key less than id0 let keys = get_keys( &[ Instruction::new(program_id, &0, vec![AccountMeta::new(id0, false)]), Instruction::new(program_id, &0, vec![AccountMeta::new(id1, false)]), ], None, ); assert_eq!(keys, InstructionKeys::new(vec![], vec![id0, id1], 0, 0)); } #[test] fn test_message_unique_keys_not_adjacent() { let program_id = Pubkey::default(); let id0 = Pubkey::default(); let id1 = Pubkey::new_rand(); let keys = get_keys( &[ Instruction::new(program_id, &0, vec![AccountMeta::new(id0, false)]), Instruction::new(program_id, &0, vec![AccountMeta::new(id1, false)]), Instruction::new(program_id, &0, vec![AccountMeta::new(id0, true)]), ], None, ); assert_eq!(keys, InstructionKeys::new(vec![id0], vec![id1], 0, 0)); } #[test] fn test_message_signed_keys_first() { let program_id = Pubkey::default(); let id0 = Pubkey::default(); let id1 = Pubkey::new_rand(); let keys = get_keys( &[ Instruction::new(program_id, &0, vec![AccountMeta::new(id0, false)]), Instruction::new(program_id, &0, vec![AccountMeta::new(id1, true)]), ], None, ); assert_eq!(keys, InstructionKeys::new(vec![id1], vec![id0], 0, 0)); } #[test] // Ensure there's a way to calculate the number of required signatures. fn test_message_signed_keys_len() { let program_id = Pubkey::default(); let id0 = Pubkey::default(); let ix = Instruction::new(program_id, &0, vec![AccountMeta::new(id0, false)]); let message = Message::new_with_payer(&[ix], None); assert_eq!(message.header.num_required_signatures, 0); let ix = Instruction::new(program_id, &0, vec![AccountMeta::new(id0, true)]); let message = Message::new(&[ix]); assert_eq!(message.header.num_required_signatures, 1); } #[test] fn test_message_readonly_keys_last() { let program_id = Pubkey::default(); let id0 = Pubkey::default(); // Identical key/program_id should be de-duped let id1 = Pubkey::new_rand(); let id2 = Pubkey::new_rand(); let id3 = Pubkey::new_rand(); let keys = get_keys( &[ Instruction::new(program_id, &0, vec![AccountMeta::new_readonly(id0, false)]), Instruction::new(program_id, &0, vec![AccountMeta::new_readonly(id1, true)]), Instruction::new(program_id, &0, vec![AccountMeta::new(id2, false)]), Instruction::new(program_id, &0, vec![AccountMeta::new(id3, true)]), ], None, ); assert_eq!( keys, InstructionKeys::new(vec![id3, id1], vec![id2, id0], 1, 1) ); } #[test] fn test_message_kitchen_sink() { let program_id0 = Pubkey::new_rand(); let program_id1 = Pubkey::new_rand(); let id0 = Pubkey::default(); let keypair1 = Keypair::new(); let id1 = keypair1.pubkey(); let message = Message::new(&[ Instruction::new(program_id0, &0, vec![AccountMeta::new(id0, false)]), Instruction::new(program_id1, &0, vec![AccountMeta::new(id1, true)]), Instruction::new(program_id0, &0, vec![AccountMeta::new(id1, false)]), ]); assert_eq!( message.instructions[0], CompiledInstruction::new(2, &0, vec![1]) ); assert_eq!( message.instructions[1], CompiledInstruction::new(3, &0, vec![0]) ); assert_eq!( message.instructions[2], CompiledInstruction::new(2, &0, vec![0]) ); } #[test] fn test_message_payer_first() { let program_id = Pubkey::default(); let payer = Pubkey::new_rand(); let id0 = Pubkey::default(); let ix = Instruction::new(program_id, &0, vec![AccountMeta::new(id0, false)]); let message = Message::new_with_payer(&[ix], Some(&payer)); assert_eq!(message.header.num_required_signatures, 1); let ix = Instruction::new(program_id, &0, vec![AccountMeta::new(id0, true)]); let message = Message::new_with_payer(&[ix], Some(&payer)); assert_eq!(message.header.num_required_signatures, 2); let ix = Instruction::new( program_id, &0, vec![AccountMeta::new(payer, true), AccountMeta::new(id0, true)], ); let message = Message::new_with_payer(&[ix], Some(&payer)); assert_eq!(message.header.num_required_signatures, 2); } #[test] fn test_message_program_last() { let program_id = Pubkey::default(); let id0 = Pubkey::new_rand(); let id1 = Pubkey::new_rand(); let keys = get_keys( &[ Instruction::new(program_id, &0, vec![AccountMeta::new_readonly(id0, false)]), Instruction::new(program_id, &0, vec![AccountMeta::new_readonly(id1, true)]), ], None, ); assert_eq!( keys, InstructionKeys::new(vec![id1], vec![id0, program_id], 1, 2) ); } #[test] fn test_program_position() { let program_id0 = Pubkey::default(); let program_id1 = Pubkey::new_rand(); let id = Pubkey::new_rand(); let message = Message::new(&[ Instruction::new(program_id0, &0, vec![AccountMeta::new(id, false)]), Instruction::new(program_id1, &0, vec![AccountMeta::new(id, true)]), ]); assert_eq!(message.program_position(0), None); assert_eq!(message.program_position(1), Some(0)); assert_eq!(message.program_position(2), Some(1)); } #[test] fn test_is_writable() { let key0 = Pubkey::new_rand(); let key1 = Pubkey::new_rand(); let key2 = Pubkey::new_rand(); let key3 = Pubkey::new_rand(); let key4 = Pubkey::new_rand(); let key5 = Pubkey::new_rand(); let message = Message { header: MessageHeader { num_required_signatures: 3, num_readonly_signed_accounts: 2, num_readonly_unsigned_accounts: 1, }, account_keys: vec![key0, key1, key2, key3, key4, key5], recent_blockhash: Hash::default(), instructions: vec![], }; assert_eq!(message.is_writable(0), true); assert_eq!(message.is_writable(1), false); assert_eq!(message.is_writable(2), false); assert_eq!(message.is_writable(3), true); assert_eq!(message.is_writable(4), true); assert_eq!(message.is_writable(5), false); } #[test] fn test_get_account_keys_by_lock_type() { let program_id = Pubkey::default(); let id0 = Pubkey::new_rand(); let id1 = Pubkey::new_rand(); let id2 = Pubkey::new_rand(); let id3 = Pubkey::new_rand(); let message = Message::new(&[ Instruction::new(program_id, &0, vec![AccountMeta::new(id0, false)]), Instruction::new(program_id, &0, vec![AccountMeta::new(id1, true)]), Instruction::new(program_id, &0, vec![AccountMeta::new_readonly(id2, false)]), Instruction::new(program_id, &0, vec![AccountMeta::new_readonly(id3, true)]), ]); assert_eq!( message.get_account_keys_by_lock_type(), (vec![&id1, &id0], vec![&id3, &id2, &program_id]) ); } }