* Add TransactionMemos column family
* Traitify extract_memos
* Write TransactionMemos in TransactionStatusService
* Populate memos from column
* Dedupe and add unit test
(cherry picked from commit 5fa3e5744c)
Co-authored-by: Tyera Eulberg <teulberg@gmail.com>
			
			
This commit is contained in:
		| @@ -7,7 +7,9 @@ use solana_ledger::{ | ||||
| use solana_runtime::bank::{ | ||||
|     Bank, InnerInstructionsList, NonceRollbackInfo, TransactionLogMessages, | ||||
| }; | ||||
| use solana_transaction_status::{InnerInstructions, Reward, TransactionStatusMeta}; | ||||
| use solana_transaction_status::{ | ||||
|     extract_and_fmt_memos, InnerInstructions, Reward, TransactionStatusMeta, | ||||
| }; | ||||
| use std::{ | ||||
|     sync::{ | ||||
|         atomic::{AtomicBool, AtomicU64, Ordering}, | ||||
| @@ -140,6 +142,12 @@ impl TransactionStatusService { | ||||
|                                 .collect(), | ||||
|                         ); | ||||
|  | ||||
|                         if let Some(memos) = extract_and_fmt_memos(transaction.message()) { | ||||
|                             blockstore | ||||
|                                 .write_transaction_memos(&transaction.signatures[0], memos) | ||||
|                                 .expect("Expect database write to succeed: TransactionMemos"); | ||||
|                         } | ||||
|  | ||||
|                         blockstore | ||||
|                             .write_transaction_status( | ||||
|                                 slot, | ||||
| @@ -158,7 +166,7 @@ impl TransactionStatusService { | ||||
|                                     rewards, | ||||
|                                 }, | ||||
|                             ) | ||||
|                             .expect("Expect database write to succeed"); | ||||
|                             .expect("Expect database write to succeed: TransactionStatus"); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|   | ||||
| @@ -139,6 +139,7 @@ pub struct Blockstore { | ||||
|     code_shred_cf: LedgerColumn<cf::ShredCode>, | ||||
|     transaction_status_cf: LedgerColumn<cf::TransactionStatus>, | ||||
|     address_signatures_cf: LedgerColumn<cf::AddressSignatures>, | ||||
|     transaction_memos_cf: LedgerColumn<cf::TransactionMemos>, | ||||
|     transaction_status_index_cf: LedgerColumn<cf::TransactionStatusIndex>, | ||||
|     active_transaction_status_index: RwLock<u64>, | ||||
|     rewards_cf: LedgerColumn<cf::Rewards>, | ||||
| @@ -313,6 +314,7 @@ impl Blockstore { | ||||
|         let code_shred_cf = db.column(); | ||||
|         let transaction_status_cf = db.column(); | ||||
|         let address_signatures_cf = db.column(); | ||||
|         let transaction_memos_cf = db.column(); | ||||
|         let transaction_status_index_cf = db.column(); | ||||
|         let rewards_cf = db.column(); | ||||
|         let blocktime_cf = db.column(); | ||||
| @@ -362,6 +364,7 @@ impl Blockstore { | ||||
|             code_shred_cf, | ||||
|             transaction_status_cf, | ||||
|             address_signatures_cf, | ||||
|             transaction_memos_cf, | ||||
|             transaction_status_index_cf, | ||||
|             active_transaction_status_index: RwLock::new(active_transaction_status_index), | ||||
|             rewards_cf, | ||||
| @@ -2016,6 +2019,14 @@ impl Blockstore { | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub fn read_transaction_memos(&self, signature: Signature) -> Result<Option<String>> { | ||||
|         self.transaction_memos_cf.get(signature) | ||||
|     } | ||||
|  | ||||
|     pub fn write_transaction_memos(&self, signature: &Signature, memos: String) -> Result<()> { | ||||
|         self.transaction_memos_cf.put(*signature, &memos) | ||||
|     } | ||||
|  | ||||
|     fn ensure_lowest_cleanup_slot(&self) -> (std::sync::RwLockReadGuard<Slot>, Slot) { | ||||
|         // Ensures consistent result by using lowest_cleanup_slot as the lower bound | ||||
|         // for reading columns that do not employ strong read consistency with slot-based | ||||
| @@ -2499,12 +2510,13 @@ impl Blockstore { | ||||
|             let transaction_status = | ||||
|                 self.get_transaction_status(signature, &confirmed_unrooted_slots)?; | ||||
|             let err = transaction_status.and_then(|(_slot, status)| status.status.err()); | ||||
|             let memo = self.read_transaction_memos(signature)?; | ||||
|             let block_time = self.get_block_time(slot)?; | ||||
|             infos.push(ConfirmedTransactionStatusWithSignature { | ||||
|                 signature, | ||||
|                 slot, | ||||
|                 err, | ||||
|                 memo: None, | ||||
|                 memo, | ||||
|                 block_time, | ||||
|             }); | ||||
|         } | ||||
|   | ||||
| @@ -61,6 +61,8 @@ const CODE_SHRED_CF: &str = "code_shred"; | ||||
| const TRANSACTION_STATUS_CF: &str = "transaction_status"; | ||||
| /// Column family for Address Signatures | ||||
| const ADDRESS_SIGNATURES_CF: &str = "address_signatures"; | ||||
| /// Column family for TransactionMemos | ||||
| const TRANSACTION_MEMOS_CF: &str = "transaction_memos"; | ||||
| /// Column family for the Transaction Status Index. | ||||
| /// This column family is used for tracking the active primary index for columns that for | ||||
| /// query performance reasons should not be indexed by Slot. | ||||
| @@ -163,6 +165,10 @@ pub mod columns { | ||||
|     /// The address signatures column | ||||
|     pub struct AddressSignatures; | ||||
|  | ||||
|     #[derive(Debug)] | ||||
|     // The transaction memos column | ||||
|     pub struct TransactionMemos; | ||||
|  | ||||
|     #[derive(Debug)] | ||||
|     /// The transaction status index column | ||||
|     pub struct TransactionStatusIndex; | ||||
| @@ -332,6 +338,10 @@ impl Rocks { | ||||
|             AddressSignatures::NAME, | ||||
|             get_cf_options::<AddressSignatures>(&access_type, &oldest_slot), | ||||
|         ); | ||||
|         let transaction_memos_cf_descriptor = ColumnFamilyDescriptor::new( | ||||
|             TransactionMemos::NAME, | ||||
|             get_cf_options::<TransactionMemos>(&access_type, &oldest_slot), | ||||
|         ); | ||||
|         let transaction_status_index_cf_descriptor = ColumnFamilyDescriptor::new( | ||||
|             TransactionStatusIndex::NAME, | ||||
|             get_cf_options::<TransactionStatusIndex>(&access_type, &oldest_slot), | ||||
| @@ -372,6 +382,7 @@ impl Rocks { | ||||
|             (ShredCode::NAME, shred_code_cf_descriptor), | ||||
|             (TransactionStatus::NAME, transaction_status_cf_descriptor), | ||||
|             (AddressSignatures::NAME, address_signatures_cf_descriptor), | ||||
|             (TransactionMemos::NAME, transaction_memos_cf_descriptor), | ||||
|             ( | ||||
|                 TransactionStatusIndex::NAME, | ||||
|                 transaction_status_index_cf_descriptor, | ||||
| @@ -494,6 +505,7 @@ impl Rocks { | ||||
|             ShredCode::NAME, | ||||
|             TransactionStatus::NAME, | ||||
|             AddressSignatures::NAME, | ||||
|             TransactionMemos::NAME, | ||||
|             TransactionStatusIndex::NAME, | ||||
|             Rewards::NAME, | ||||
|             Blocktime::NAME, | ||||
| @@ -589,6 +601,10 @@ impl TypedColumn for columns::AddressSignatures { | ||||
|     type Type = blockstore_meta::AddressSignatureMeta; | ||||
| } | ||||
|  | ||||
| impl TypedColumn for columns::TransactionMemos { | ||||
|     type Type = String; | ||||
| } | ||||
|  | ||||
| impl TypedColumn for columns::TransactionStatusIndex { | ||||
|     type Type = blockstore_meta::TransactionStatusIndexMeta; | ||||
| } | ||||
| @@ -703,6 +719,37 @@ impl ColumnName for columns::AddressSignatures { | ||||
|     const NAME: &'static str = ADDRESS_SIGNATURES_CF; | ||||
| } | ||||
|  | ||||
| impl Column for columns::TransactionMemos { | ||||
|     type Index = Signature; | ||||
|  | ||||
|     fn key(signature: Signature) -> Vec<u8> { | ||||
|         let mut key = vec![0; 64]; // size_of Signature | ||||
|         key[0..64].clone_from_slice(&signature.as_ref()[0..64]); | ||||
|         key | ||||
|     } | ||||
|  | ||||
|     fn index(key: &[u8]) -> Signature { | ||||
|         Signature::new(&key[0..64]) | ||||
|     } | ||||
|  | ||||
|     fn primary_index(_index: Self::Index) -> u64 { | ||||
|         unimplemented!() | ||||
|     } | ||||
|  | ||||
|     fn slot(_index: Self::Index) -> Slot { | ||||
|         unimplemented!() | ||||
|     } | ||||
|  | ||||
|     #[allow(clippy::wrong_self_convention)] | ||||
|     fn as_index(_index: u64) -> Self::Index { | ||||
|         Signature::default() | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ColumnName for columns::TransactionMemos { | ||||
|     const NAME: &'static str = TRANSACTION_MEMOS_CF; | ||||
| } | ||||
|  | ||||
| impl Column for columns::TransactionStatusIndex { | ||||
|     type Index = u64; | ||||
|  | ||||
| @@ -1364,6 +1411,7 @@ fn excludes_from_compaction(cf_name: &str) -> bool { | ||||
|     let no_compaction_cfs: HashSet<&'static str> = vec![ | ||||
|         columns::TransactionStatusIndex::NAME, | ||||
|         columns::ProgramCosts::NAME, | ||||
|         columns::TransactionMemos::NAME, | ||||
|     ] | ||||
|     .into_iter() | ||||
|     .collect(); | ||||
| @@ -1431,6 +1479,7 @@ pub mod tests { | ||||
|             columns::TransactionStatusIndex::NAME | ||||
|         )); | ||||
|         assert!(excludes_from_compaction(columns::ProgramCosts::NAME)); | ||||
|         assert!(excludes_from_compaction(columns::TransactionMemos::NAME)); | ||||
|         assert!(!excludes_from_compaction("something else")); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -15,8 +15,8 @@ pub fn spl_memo_id_v3() -> Pubkey { | ||||
|     Pubkey::new_from_array(spl_memo::id().to_bytes()) | ||||
| } | ||||
|  | ||||
| pub fn extract_and_fmt_memos(message: &Message) -> Option<String> { | ||||
|     let memos = extract_memos(message); | ||||
| pub fn extract_and_fmt_memos<T: ExtractMemos>(message: &T) -> Option<String> { | ||||
|     let memos = message.extract_memos(); | ||||
|     if memos.is_empty() { | ||||
|         None | ||||
|     } else { | ||||
| @@ -24,20 +24,80 @@ pub fn extract_and_fmt_memos(message: &Message) -> Option<String> { | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn extract_memos(message: &Message) -> Vec<String> { | ||||
|     let mut memos = vec![]; | ||||
|     if message.account_keys.contains(&spl_memo_id_v1()) | ||||
|         || message.account_keys.contains(&spl_memo_id_v3()) | ||||
|     { | ||||
|         for instruction in &message.instructions { | ||||
|             let program_id = message.account_keys[instruction.program_id_index as usize]; | ||||
| fn maybe_push_parsed_memo(memos: &mut Vec<String>, program_id: Pubkey, data: &[u8]) { | ||||
|     if program_id == spl_memo_id_v1() || program_id == spl_memo_id_v3() { | ||||
|                 let memo_len = instruction.data.len(); | ||||
|                 let parsed_memo = parse_memo_data(&instruction.data) | ||||
|                     .unwrap_or_else(|_| "(unparseable)".to_string()); | ||||
|         let memo_len = data.len(); | ||||
|         let parsed_memo = parse_memo_data(data).unwrap_or_else(|_| "(unparseable)".to_string()); | ||||
|         memos.push(format!("[{}] {}", memo_len, parsed_memo)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub trait ExtractMemos { | ||||
|     fn extract_memos(&self) -> Vec<String>; | ||||
| } | ||||
|  | ||||
| impl ExtractMemos for Message { | ||||
|     fn extract_memos(&self) -> Vec<String> { | ||||
|         let mut memos = vec![]; | ||||
|         if self.account_keys.contains(&spl_memo_id_v1()) | ||||
|             || self.account_keys.contains(&spl_memo_id_v3()) | ||||
|         { | ||||
|             for instruction in &self.instructions { | ||||
|                 let program_id = self.account_keys[instruction.program_id_index as usize]; | ||||
|                 maybe_push_parsed_memo(&mut memos, program_id, &instruction.data); | ||||
|             } | ||||
|         } | ||||
|         memos | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod test { | ||||
|     use { | ||||
|         super::*, | ||||
|         solana_sdk::{hash::Hash, instruction::CompiledInstruction}, | ||||
|     }; | ||||
|  | ||||
|     #[test] | ||||
|     fn test_extract_memos() { | ||||
|         let fee_payer = Pubkey::new_unique(); | ||||
|         let another_program_id = Pubkey::new_unique(); | ||||
|         let memo0 = "Test memo"; | ||||
|         let memo1 = "🦖"; | ||||
|         let expected_memos = vec![ | ||||
|             format!("[{}] {}", memo0.len(), memo0), | ||||
|             format!("[{}] {}", memo1.len(), memo1), | ||||
|         ]; | ||||
|         let memo_instructions = vec![ | ||||
|             CompiledInstruction { | ||||
|                 program_id_index: 1, | ||||
|                 accounts: vec![], | ||||
|                 data: memo0.as_bytes().to_vec(), | ||||
|             }, | ||||
|             CompiledInstruction { | ||||
|                 program_id_index: 2, | ||||
|                 accounts: vec![], | ||||
|                 data: memo1.as_bytes().to_vec(), | ||||
|             }, | ||||
|             CompiledInstruction { | ||||
|                 program_id_index: 3, | ||||
|                 accounts: vec![], | ||||
|                 data: memo1.as_bytes().to_vec(), | ||||
|             }, | ||||
|         ]; | ||||
|         let message = Message::new_with_compiled_instructions( | ||||
|             1, | ||||
|             0, | ||||
|             3, | ||||
|             vec![ | ||||
|                 fee_payer, | ||||
|                 spl_memo_id_v1(), | ||||
|                 another_program_id, | ||||
|                 spl_memo_id_v3(), | ||||
|             ], | ||||
|             Hash::default(), | ||||
|             memo_instructions, | ||||
|         ); | ||||
|         assert_eq!(message.extract_memos(), expected_memos); | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user