* 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::{ | use solana_runtime::bank::{ | ||||||
|     Bank, InnerInstructionsList, NonceRollbackInfo, TransactionLogMessages, |     Bank, InnerInstructionsList, NonceRollbackInfo, TransactionLogMessages, | ||||||
| }; | }; | ||||||
| use solana_transaction_status::{InnerInstructions, Reward, TransactionStatusMeta}; | use solana_transaction_status::{ | ||||||
|  |     extract_and_fmt_memos, InnerInstructions, Reward, TransactionStatusMeta, | ||||||
|  | }; | ||||||
| use std::{ | use std::{ | ||||||
|     sync::{ |     sync::{ | ||||||
|         atomic::{AtomicBool, AtomicU64, Ordering}, |         atomic::{AtomicBool, AtomicU64, Ordering}, | ||||||
| @@ -140,6 +142,12 @@ impl TransactionStatusService { | |||||||
|                                 .collect(), |                                 .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 |                         blockstore | ||||||
|                             .write_transaction_status( |                             .write_transaction_status( | ||||||
|                                 slot, |                                 slot, | ||||||
| @@ -158,7 +166,7 @@ impl TransactionStatusService { | |||||||
|                                     rewards, |                                     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>, |     code_shred_cf: LedgerColumn<cf::ShredCode>, | ||||||
|     transaction_status_cf: LedgerColumn<cf::TransactionStatus>, |     transaction_status_cf: LedgerColumn<cf::TransactionStatus>, | ||||||
|     address_signatures_cf: LedgerColumn<cf::AddressSignatures>, |     address_signatures_cf: LedgerColumn<cf::AddressSignatures>, | ||||||
|  |     transaction_memos_cf: LedgerColumn<cf::TransactionMemos>, | ||||||
|     transaction_status_index_cf: LedgerColumn<cf::TransactionStatusIndex>, |     transaction_status_index_cf: LedgerColumn<cf::TransactionStatusIndex>, | ||||||
|     active_transaction_status_index: RwLock<u64>, |     active_transaction_status_index: RwLock<u64>, | ||||||
|     rewards_cf: LedgerColumn<cf::Rewards>, |     rewards_cf: LedgerColumn<cf::Rewards>, | ||||||
| @@ -313,6 +314,7 @@ impl Blockstore { | |||||||
|         let code_shred_cf = db.column(); |         let code_shred_cf = db.column(); | ||||||
|         let transaction_status_cf = db.column(); |         let transaction_status_cf = db.column(); | ||||||
|         let address_signatures_cf = db.column(); |         let address_signatures_cf = db.column(); | ||||||
|  |         let transaction_memos_cf = db.column(); | ||||||
|         let transaction_status_index_cf = db.column(); |         let transaction_status_index_cf = db.column(); | ||||||
|         let rewards_cf = db.column(); |         let rewards_cf = db.column(); | ||||||
|         let blocktime_cf = db.column(); |         let blocktime_cf = db.column(); | ||||||
| @@ -362,6 +364,7 @@ impl Blockstore { | |||||||
|             code_shred_cf, |             code_shred_cf, | ||||||
|             transaction_status_cf, |             transaction_status_cf, | ||||||
|             address_signatures_cf, |             address_signatures_cf, | ||||||
|  |             transaction_memos_cf, | ||||||
|             transaction_status_index_cf, |             transaction_status_index_cf, | ||||||
|             active_transaction_status_index: RwLock::new(active_transaction_status_index), |             active_transaction_status_index: RwLock::new(active_transaction_status_index), | ||||||
|             rewards_cf, |             rewards_cf, | ||||||
| @@ -2016,6 +2019,14 @@ impl Blockstore { | |||||||
|         Ok(()) |         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) { |     fn ensure_lowest_cleanup_slot(&self) -> (std::sync::RwLockReadGuard<Slot>, Slot) { | ||||||
|         // Ensures consistent result by using lowest_cleanup_slot as the lower bound |         // 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 |         // for reading columns that do not employ strong read consistency with slot-based | ||||||
| @@ -2499,12 +2510,13 @@ impl Blockstore { | |||||||
|             let transaction_status = |             let transaction_status = | ||||||
|                 self.get_transaction_status(signature, &confirmed_unrooted_slots)?; |                 self.get_transaction_status(signature, &confirmed_unrooted_slots)?; | ||||||
|             let err = transaction_status.and_then(|(_slot, status)| status.status.err()); |             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)?; |             let block_time = self.get_block_time(slot)?; | ||||||
|             infos.push(ConfirmedTransactionStatusWithSignature { |             infos.push(ConfirmedTransactionStatusWithSignature { | ||||||
|                 signature, |                 signature, | ||||||
|                 slot, |                 slot, | ||||||
|                 err, |                 err, | ||||||
|                 memo: None, |                 memo, | ||||||
|                 block_time, |                 block_time, | ||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -61,6 +61,8 @@ const CODE_SHRED_CF: &str = "code_shred"; | |||||||
| const TRANSACTION_STATUS_CF: &str = "transaction_status"; | const TRANSACTION_STATUS_CF: &str = "transaction_status"; | ||||||
| /// Column family for Address Signatures | /// Column family for Address Signatures | ||||||
| const ADDRESS_SIGNATURES_CF: &str = "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. | /// Column family for the Transaction Status Index. | ||||||
| /// This column family is used for tracking the active primary index for columns that for | /// This column family is used for tracking the active primary index for columns that for | ||||||
| /// query performance reasons should not be indexed by Slot. | /// query performance reasons should not be indexed by Slot. | ||||||
| @@ -163,6 +165,10 @@ pub mod columns { | |||||||
|     /// The address signatures column |     /// The address signatures column | ||||||
|     pub struct AddressSignatures; |     pub struct AddressSignatures; | ||||||
|  |  | ||||||
|  |     #[derive(Debug)] | ||||||
|  |     // The transaction memos column | ||||||
|  |     pub struct TransactionMemos; | ||||||
|  |  | ||||||
|     #[derive(Debug)] |     #[derive(Debug)] | ||||||
|     /// The transaction status index column |     /// The transaction status index column | ||||||
|     pub struct TransactionStatusIndex; |     pub struct TransactionStatusIndex; | ||||||
| @@ -332,6 +338,10 @@ impl Rocks { | |||||||
|             AddressSignatures::NAME, |             AddressSignatures::NAME, | ||||||
|             get_cf_options::<AddressSignatures>(&access_type, &oldest_slot), |             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( |         let transaction_status_index_cf_descriptor = ColumnFamilyDescriptor::new( | ||||||
|             TransactionStatusIndex::NAME, |             TransactionStatusIndex::NAME, | ||||||
|             get_cf_options::<TransactionStatusIndex>(&access_type, &oldest_slot), |             get_cf_options::<TransactionStatusIndex>(&access_type, &oldest_slot), | ||||||
| @@ -372,6 +382,7 @@ impl Rocks { | |||||||
|             (ShredCode::NAME, shred_code_cf_descriptor), |             (ShredCode::NAME, shred_code_cf_descriptor), | ||||||
|             (TransactionStatus::NAME, transaction_status_cf_descriptor), |             (TransactionStatus::NAME, transaction_status_cf_descriptor), | ||||||
|             (AddressSignatures::NAME, address_signatures_cf_descriptor), |             (AddressSignatures::NAME, address_signatures_cf_descriptor), | ||||||
|  |             (TransactionMemos::NAME, transaction_memos_cf_descriptor), | ||||||
|             ( |             ( | ||||||
|                 TransactionStatusIndex::NAME, |                 TransactionStatusIndex::NAME, | ||||||
|                 transaction_status_index_cf_descriptor, |                 transaction_status_index_cf_descriptor, | ||||||
| @@ -494,6 +505,7 @@ impl Rocks { | |||||||
|             ShredCode::NAME, |             ShredCode::NAME, | ||||||
|             TransactionStatus::NAME, |             TransactionStatus::NAME, | ||||||
|             AddressSignatures::NAME, |             AddressSignatures::NAME, | ||||||
|  |             TransactionMemos::NAME, | ||||||
|             TransactionStatusIndex::NAME, |             TransactionStatusIndex::NAME, | ||||||
|             Rewards::NAME, |             Rewards::NAME, | ||||||
|             Blocktime::NAME, |             Blocktime::NAME, | ||||||
| @@ -589,6 +601,10 @@ impl TypedColumn for columns::AddressSignatures { | |||||||
|     type Type = blockstore_meta::AddressSignatureMeta; |     type Type = blockstore_meta::AddressSignatureMeta; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl TypedColumn for columns::TransactionMemos { | ||||||
|  |     type Type = String; | ||||||
|  | } | ||||||
|  |  | ||||||
| impl TypedColumn for columns::TransactionStatusIndex { | impl TypedColumn for columns::TransactionStatusIndex { | ||||||
|     type Type = blockstore_meta::TransactionStatusIndexMeta; |     type Type = blockstore_meta::TransactionStatusIndexMeta; | ||||||
| } | } | ||||||
| @@ -703,6 +719,37 @@ impl ColumnName for columns::AddressSignatures { | |||||||
|     const NAME: &'static str = ADDRESS_SIGNATURES_CF; |     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 { | impl Column for columns::TransactionStatusIndex { | ||||||
|     type Index = u64; |     type Index = u64; | ||||||
|  |  | ||||||
| @@ -1364,6 +1411,7 @@ fn excludes_from_compaction(cf_name: &str) -> bool { | |||||||
|     let no_compaction_cfs: HashSet<&'static str> = vec![ |     let no_compaction_cfs: HashSet<&'static str> = vec![ | ||||||
|         columns::TransactionStatusIndex::NAME, |         columns::TransactionStatusIndex::NAME, | ||||||
|         columns::ProgramCosts::NAME, |         columns::ProgramCosts::NAME, | ||||||
|  |         columns::TransactionMemos::NAME, | ||||||
|     ] |     ] | ||||||
|     .into_iter() |     .into_iter() | ||||||
|     .collect(); |     .collect(); | ||||||
| @@ -1431,6 +1479,7 @@ pub mod tests { | |||||||
|             columns::TransactionStatusIndex::NAME |             columns::TransactionStatusIndex::NAME | ||||||
|         )); |         )); | ||||||
|         assert!(excludes_from_compaction(columns::ProgramCosts::NAME)); |         assert!(excludes_from_compaction(columns::ProgramCosts::NAME)); | ||||||
|  |         assert!(excludes_from_compaction(columns::TransactionMemos::NAME)); | ||||||
|         assert!(!excludes_from_compaction("something else")); |         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()) |     Pubkey::new_from_array(spl_memo::id().to_bytes()) | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn extract_and_fmt_memos(message: &Message) -> Option<String> { | pub fn extract_and_fmt_memos<T: ExtractMemos>(message: &T) -> Option<String> { | ||||||
|     let memos = extract_memos(message); |     let memos = message.extract_memos(); | ||||||
|     if memos.is_empty() { |     if memos.is_empty() { | ||||||
|         None |         None | ||||||
|     } else { |     } else { | ||||||
| @@ -24,20 +24,80 @@ pub fn extract_and_fmt_memos(message: &Message) -> Option<String> { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| fn extract_memos(message: &Message) -> Vec<String> { | fn maybe_push_parsed_memo(memos: &mut Vec<String>, program_id: Pubkey, data: &[u8]) { | ||||||
|     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]; |  | ||||||
|     if program_id == spl_memo_id_v1() || program_id == spl_memo_id_v3() { |     if program_id == spl_memo_id_v1() || program_id == spl_memo_id_v3() { | ||||||
|                 let memo_len = instruction.data.len(); |         let memo_len = data.len(); | ||||||
|                 let parsed_memo = parse_memo_data(&instruction.data) |         let parsed_memo = parse_memo_data(data).unwrap_or_else(|_| "(unparseable)".to_string()); | ||||||
|                     .unwrap_or_else(|_| "(unparseable)".to_string()); |  | ||||||
|         memos.push(format!("[{}] {}", memo_len, parsed_memo)); |         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 |         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