diff --git a/core/src/transaction_status_service.rs b/core/src/transaction_status_service.rs index fbdac8721f7c27..b287a6b58a9c0b 100644 --- a/core/src/transaction_status_service.rs +++ b/core/src/transaction_status_service.rs @@ -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.signature(), 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"); } } } diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index 12d890730c550b..26311708f493f0 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -139,6 +139,7 @@ pub struct Blockstore { code_shred_cf: LedgerColumn, transaction_status_cf: LedgerColumn, address_signatures_cf: LedgerColumn, + transaction_memos_cf: LedgerColumn, transaction_status_index_cf: LedgerColumn, active_transaction_status_index: RwLock, rewards_cf: LedgerColumn, @@ -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> { + 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) { // 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, }); } diff --git a/ledger/src/blockstore_db.rs b/ledger/src/blockstore_db.rs index ba43ae26f5ef95..30f3856562df12 100644 --- a/ledger/src/blockstore_db.rs +++ b/ledger/src/blockstore_db.rs @@ -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::(&access_type, &oldest_slot), ); + let transaction_memos_cf_descriptor = ColumnFamilyDescriptor::new( + TransactionMemos::NAME, + get_cf_options::(&access_type, &oldest_slot), + ); let transaction_status_index_cf_descriptor = ColumnFamilyDescriptor::new( TransactionStatusIndex::NAME, get_cf_options::(&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 { + 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")); } } diff --git a/transaction-status/src/extract_memos.rs b/transaction-status/src/extract_memos.rs index 2e7eb540b63d16..87f5bd58b6c178 100644 --- a/transaction-status/src/extract_memos.rs +++ b/transaction-status/src/extract_memos.rs @@ -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 { - let memos = extract_memos(message); +pub fn extract_and_fmt_memos(message: &T) -> Option { + let memos = message.extract_memos(); if memos.is_empty() { None } else { @@ -24,20 +24,83 @@ pub fn extract_and_fmt_memos(message: &Message) -> Option { } } -fn extract_memos(message: &Message) -> Vec { - 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() { - let memo_len = instruction.data.len(); - let parsed_memo = parse_memo_data(&instruction.data) - .unwrap_or_else(|_| "(unparseable)".to_string()); - memos.push(format!("[{}] {}", memo_len, parsed_memo)); +fn maybe_push_parsed_memo(memos: &mut Vec, program_id: Pubkey, data: &[u8]) { + if program_id == spl_memo_id_v1() || program_id == spl_memo_id_v3() { + 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; +} + +impl ExtractMemos for Message { + fn extract_memos(&self) -> Vec { + 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.clone(), + ); + assert_eq!(message.extract_memos(), expected_memos); } - memos }