* 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 program_id == spl_memo_id_v1() || program_id == spl_memo_id_v3() {
|
||||||
if message.account_keys.contains(&spl_memo_id_v1())
|
let memo_len = data.len();
|
||||||
|| message.account_keys.contains(&spl_memo_id_v3())
|
let parsed_memo = parse_memo_data(data).unwrap_or_else(|_| "(unparseable)".to_string());
|
||||||
{
|
memos.push(format!("[{}] {}", memo_len, parsed_memo));
|
||||||
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();
|
pub trait ExtractMemos {
|
||||||
let parsed_memo = parse_memo_data(&instruction.data)
|
fn extract_memos(&self) -> Vec<String>;
|
||||||
.unwrap_or_else(|_| "(unparseable)".to_string());
|
}
|
||||||
memos.push(format!("[{}] {}", memo_len, parsed_memo));
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
memos
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user