Add `solana logs` command

This commit is contained in:
Michael Vines 2020-11-20 13:52:58 -08:00
parent f96c4ec84e
commit 4ef2da0ff0
10 changed files with 785 additions and 105 deletions

View File

@ -189,6 +189,7 @@ pub fn commitment_of(matches: &ArgMatches<'_>, name: &str) -> Option<CommitmentC
"recent" => CommitmentConfig::recent(), "recent" => CommitmentConfig::recent(),
"root" => CommitmentConfig::root(), "root" => CommitmentConfig::root(),
"single" => CommitmentConfig::single(), "single" => CommitmentConfig::single(),
"singleGossip" => CommitmentConfig::single_gossip(),
_ => CommitmentConfig::default(), _ => CommitmentConfig::default(),
}) })
} }

View File

@ -31,7 +31,7 @@ use solana_client::{
client_error::{ClientError, ClientErrorKind, Result as ClientResult}, client_error::{ClientError, ClientErrorKind, Result as ClientResult},
nonce_utils, nonce_utils,
rpc_client::RpcClient, rpc_client::RpcClient,
rpc_config::{RpcLargestAccountsFilter, RpcSendTransactionConfig}, rpc_config::{RpcLargestAccountsFilter, RpcSendTransactionConfig, RpcTransactionLogsFilter},
rpc_request::MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS, rpc_request::MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS,
rpc_response::{RpcKeyedAccount, RpcLeaderSchedule}, rpc_response::{RpcKeyedAccount, RpcLeaderSchedule},
}; };
@ -120,6 +120,9 @@ pub enum CliCommand {
}, },
LeaderSchedule, LeaderSchedule,
LiveSlots, LiveSlots,
Logs {
filter: RpcTransactionLogsFilter,
},
Ping { Ping {
lamports: u64, lamports: u64,
interval: Duration, interval: Duration,
@ -585,6 +588,7 @@ pub fn parse_command(
command: CliCommand::LiveSlots, command: CliCommand::LiveSlots,
signers: vec![], signers: vec![],
}), }),
("logs", Some(matches)) => parse_logs(matches, wallet_manager),
("block-production", Some(matches)) => parse_show_block_production(matches), ("block-production", Some(matches)) => parse_show_block_production(matches),
("gossip", Some(_matches)) => Ok(CliCommandInfo { ("gossip", Some(_matches)) => Ok(CliCommandInfo {
command: CliCommand::ShowGossip, command: CliCommand::ShowGossip,
@ -1559,7 +1563,8 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
process_inflation_subcommand(&rpc_client, config, inflation_subcommand) process_inflation_subcommand(&rpc_client, config, inflation_subcommand)
} }
CliCommand::LeaderSchedule => process_leader_schedule(&rpc_client), CliCommand::LeaderSchedule => process_leader_schedule(&rpc_client),
CliCommand::LiveSlots => process_live_slots(&config.websocket_url), CliCommand::LiveSlots => process_live_slots(&config),
CliCommand::Logs { filter } => process_logs(&config, filter),
CliCommand::Ping { CliCommand::Ping {
lamports, lamports,
interval, interval,

View File

@ -7,7 +7,10 @@ use chrono::{Local, TimeZone};
use clap::{value_t, value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand}; use clap::{value_t, value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand};
use console::{style, Emoji}; use console::{style, Emoji};
use solana_clap_utils::{ use solana_clap_utils::{
commitment::commitment_arg, input_parsers::*, input_validators::*, keypair::DefaultSigner, commitment::{commitment_arg, commitment_arg_with_default},
input_parsers::*,
input_validators::*,
keypair::DefaultSigner,
}; };
use solana_cli_output::{ use solana_cli_output::{
display::{ display::{
@ -21,7 +24,7 @@ use solana_client::{
rpc_client::{GetConfirmedSignaturesForAddress2Config, RpcClient}, rpc_client::{GetConfirmedSignaturesForAddress2Config, RpcClient},
rpc_config::{ rpc_config::{
RpcAccountInfoConfig, RpcLargestAccountsConfig, RpcLargestAccountsFilter, RpcAccountInfoConfig, RpcLargestAccountsConfig, RpcLargestAccountsFilter,
RpcProgramAccountsConfig, RpcProgramAccountsConfig, RpcTransactionLogsConfig, RpcTransactionLogsFilter,
}, },
rpc_filter, rpc_filter,
rpc_response::SlotInfo, rpc_response::SlotInfo,
@ -233,6 +236,26 @@ impl ClusterQuerySubCommands for App<'_, '_> {
SubCommand::with_name("live-slots") SubCommand::with_name("live-slots")
.about("Show information about the current slot progression"), .about("Show information about the current slot progression"),
) )
.subcommand(
SubCommand::with_name("logs")
.about("Stream transaction logs")
.arg(
pubkey!(Arg::with_name("address")
.index(1)
.value_name("ADDRESS"),
"Account address to monitor \
[default: monitor all transactions except for votes] \
")
)
.arg(
Arg::with_name("include_votes")
.long("include-votes")
.takes_value(false)
.conflicts_with("address")
.help("Include vote transactions when monitoring all transactions")
)
.arg(commitment_arg_with_default("singleGossip")),
)
.subcommand( .subcommand(
SubCommand::with_name("block-production") SubCommand::with_name("block-production")
.about("Show information about block production") .about("Show information about block production")
@ -1172,24 +1195,83 @@ pub fn process_ping(
Ok("".to_string()) Ok("".to_string())
} }
pub fn process_live_slots(url: &str) -> ProcessResult { pub fn parse_logs(
let exit = Arc::new(AtomicBool::new(false)); matches: &ArgMatches<'_>,
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
let address = pubkey_of_signer(matches, "address", wallet_manager)?;
let include_votes = matches.is_present("include_votes");
// Disable Ctrl+C handler as sometimes the PubsubClient shutdown can stall. Also it doesn't let filter = match address {
// really matter that the shutdown is clean because the process is terminating. None => {
/* if include_votes {
let exit_clone = exit.clone(); RpcTransactionLogsFilter::AllWithVotes
ctrlc::set_handler(move || { } else {
exit_clone.store(true, Ordering::Relaxed); RpcTransactionLogsFilter::All
})?; }
*/ }
Some(address) => RpcTransactionLogsFilter::Mentions(vec![address.to_string()]),
};
Ok(CliCommandInfo {
command: CliCommand::Logs { filter },
signers: vec![],
})
}
pub fn process_logs(config: &CliConfig, filter: &RpcTransactionLogsFilter) -> ProcessResult {
println!(
"Streaming transaction logs{}. {:?} commitment",
match filter {
RpcTransactionLogsFilter::All => "".into(),
RpcTransactionLogsFilter::AllWithVotes => " (including votes)".into(),
RpcTransactionLogsFilter::Mentions(addresses) =>
format!(" mentioning {}", addresses.join(",")),
},
config.commitment.commitment
);
let (_client, receiver) = PubsubClient::logs_subscribe(
&config.websocket_url,
filter.clone(),
RpcTransactionLogsConfig {
commitment: Some(config.commitment),
},
)?;
loop {
match receiver.recv() {
Ok(logs) => {
println!("Transaction executed in slot {}:", logs.context.slot);
println!(" Signature: {}", logs.value.signature);
println!(
" Status: {}",
logs.value
.err
.map(|err| err.to_string())
.unwrap_or_else(|| "Ok".to_string())
);
println!(" Log Messages:");
for log in logs.value.logs {
println!(" {}", log);
}
}
Err(err) => {
return Ok(format!("Disconnected: {}", err));
}
}
}
}
pub fn process_live_slots(config: &CliConfig) -> ProcessResult {
let exit = Arc::new(AtomicBool::new(false));
let mut current: Option<SlotInfo> = None; let mut current: Option<SlotInfo> = None;
let mut message = "".to_string(); let mut message = "".to_string();
let slot_progress = new_spinner_progress_bar(); let slot_progress = new_spinner_progress_bar();
slot_progress.set_message("Connecting..."); slot_progress.set_message("Connecting...");
let (mut client, receiver) = PubsubClient::slot_subscribe(url)?; let (mut client, receiver) = PubsubClient::slot_subscribe(&config.websocket_url)?;
slot_progress.set_message("Connected."); slot_progress.set_message("Connected.");
let spacer = "|"; let spacer = "|";

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
rpc_config::RpcSignatureSubscribeConfig, rpc_config::{RpcSignatureSubscribeConfig, RpcTransactionLogsConfig, RpcTransactionLogsFilter},
rpc_response::{Response as RpcResponse, RpcSignatureResult, SlotInfo}, rpc_response::{Response as RpcResponse, RpcLogsResponse, RpcSignatureResult, SlotInfo},
}; };
use log::*; use log::*;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
@ -23,8 +23,6 @@ use thiserror::Error;
use tungstenite::{client::AutoStream, connect, Message, WebSocket}; use tungstenite::{client::AutoStream, connect, Message, WebSocket};
use url::{ParseError, Url}; use url::{ParseError, Url};
type PubsubSignatureResponse = PubsubClientSubscription<RpcResponse<RpcSignatureResult>>;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum PubsubClientError { pub enum PubsubClientError {
#[error("url parse error")] #[error("url parse error")]
@ -36,8 +34,8 @@ pub enum PubsubClientError {
#[error("json parse error")] #[error("json parse error")]
JsonParseError(#[from] serde_json::error::Error), JsonParseError(#[from] serde_json::error::Error),
#[error("unexpected message format")] #[error("unexpected message format: {0}")]
UnexpectedMessageError, UnexpectedMessageError(String),
} }
pub struct PubsubClientSubscription<T> pub struct PubsubClientSubscription<T>
@ -92,8 +90,11 @@ where
return Ok(x); return Ok(x);
} }
} }
// TODO: Add proper JSON RPC response/error handling...
Err(PubsubClientError::UnexpectedMessageError) Err(PubsubClientError::UnexpectedMessageError(format!(
"{:?}",
json_msg
)))
} }
pub fn send_unsubscribe(&self) -> Result<(), PubsubClientError> { pub fn send_unsubscribe(&self) -> Result<(), PubsubClientError> {
@ -117,14 +118,18 @@ where
let message_text = &message.into_text().unwrap(); let message_text = &message.into_text().unwrap();
let json_msg: Map<String, Value> = serde_json::from_str(message_text)?; let json_msg: Map<String, Value> = serde_json::from_str(message_text)?;
if let Some(Object(value_1)) = json_msg.get("params") { if let Some(Object(params)) = json_msg.get("params") {
if let Some(value_2) = value_1.get("result") { if let Some(result) = params.get("result") {
let x: T = serde_json::from_value::<T>(value_2.clone()).unwrap(); let x: T = serde_json::from_value::<T>(result.clone()).unwrap();
return Ok(x); return Ok(x);
} }
} }
Err(PubsubClientError::UnexpectedMessageError) // TODO: Add proper JSON RPC response/error handling...
Err(PubsubClientError::UnexpectedMessageError(format!(
"{:?}",
json_msg
)))
} }
pub fn shutdown(&mut self) -> std::thread::Result<()> { pub fn shutdown(&mut self) -> std::thread::Result<()> {
@ -141,15 +146,79 @@ where
} }
} }
const SLOT_OPERATION: &str = "slot"; pub type LogsSubscription = (
const SIGNATURE_OPERATION: &str = "signature"; PubsubClientSubscription<RpcResponse<RpcLogsResponse>>,
Receiver<RpcResponse<RpcLogsResponse>>,
);
pub type SlotsSubscription = (PubsubClientSubscription<SlotInfo>, Receiver<SlotInfo>);
pub type SignatureSubscription = (
PubsubClientSubscription<RpcResponse<RpcSignatureResult>>,
Receiver<RpcResponse<RpcSignatureResult>>,
);
pub struct PubsubClient {} pub struct PubsubClient {}
impl PubsubClient { impl PubsubClient {
pub fn slot_subscribe( pub fn logs_subscribe(
url: &str, url: &str,
) -> Result<(PubsubClientSubscription<SlotInfo>, Receiver<SlotInfo>), PubsubClientError> { filter: RpcTransactionLogsFilter,
config: RpcTransactionLogsConfig,
) -> Result<LogsSubscription, PubsubClientError> {
let url = Url::parse(url)?;
let (socket, _response) = connect(url)?;
let (sender, receiver) = channel();
let socket = Arc::new(RwLock::new(socket));
let socket_clone = socket.clone();
let exit = Arc::new(AtomicBool::new(false));
let exit_clone = exit.clone();
let subscription_id =
PubsubClientSubscription::<RpcResponse<RpcLogsResponse>>::send_subscribe(
&socket_clone,
json!({
"jsonrpc":"2.0","id":1,"method":"logsSubscribe","params":[filter, config]
})
.to_string(),
)?;
let t_cleanup = std::thread::spawn(move || {
loop {
if exit_clone.load(Ordering::Relaxed) {
break;
}
match PubsubClientSubscription::read_message(&socket_clone) {
Ok(message) => match sender.send(message) {
Ok(_) => (),
Err(err) => {
info!("receive error: {:?}", err);
break;
}
},
Err(err) => {
info!("receive error: {:?}", err);
break;
}
}
}
info!("websocket - exited receive loop");
});
let result = PubsubClientSubscription {
message_type: PhantomData,
operation: "logs",
socket,
subscription_id,
t_cleanup: Some(t_cleanup),
exit,
};
Ok((result, receiver))
}
pub fn slot_subscribe(url: &str) -> Result<SlotsSubscription, PubsubClientError> {
let url = Url::parse(url)?; let url = Url::parse(url)?;
let (socket, _response) = connect(url)?; let (socket, _response) = connect(url)?;
let (sender, receiver) = channel::<SlotInfo>(); let (sender, receiver) = channel::<SlotInfo>();
@ -161,41 +230,37 @@ impl PubsubClient {
let subscription_id = PubsubClientSubscription::<SlotInfo>::send_subscribe( let subscription_id = PubsubClientSubscription::<SlotInfo>::send_subscribe(
&socket_clone, &socket_clone,
json!({ json!({
"jsonrpc":"2.0","id":1,"method":format!("{}Subscribe", SLOT_OPERATION),"params":[] "jsonrpc":"2.0","id":1,"method":"slotSubscribe","params":[]
}) })
.to_string(), .to_string(),
) )?;
.unwrap();
let t_cleanup = std::thread::spawn(move || { let t_cleanup = std::thread::spawn(move || {
loop { loop {
if exit_clone.load(Ordering::Relaxed) { if exit_clone.load(Ordering::Relaxed) {
break; break;
} }
match PubsubClientSubscription::read_message(&socket_clone) {
let message: Result<SlotInfo, PubsubClientError> = Ok(message) => match sender.send(message) {
PubsubClientSubscription::read_message(&socket_clone);
if let Ok(msg) = message {
match sender.send(msg) {
Ok(_) => (), Ok(_) => (),
Err(err) => { Err(err) => {
info!("receive error: {:?}", err); info!("receive error: {:?}", err);
break; break;
} }
},
Err(err) => {
info!("receive error: {:?}", err);
break;
} }
} else {
info!("receive error: {:?}", message);
break;
} }
} }
info!("websocket - exited receive loop"); info!("websocket - exited receive loop");
}); });
let result: PubsubClientSubscription<SlotInfo> = PubsubClientSubscription { let result = PubsubClientSubscription {
message_type: PhantomData, message_type: PhantomData,
operation: SLOT_OPERATION, operation: "slot",
socket, socket,
subscription_id, subscription_id,
t_cleanup: Some(t_cleanup), t_cleanup: Some(t_cleanup),
@ -209,16 +274,10 @@ impl PubsubClient {
url: &str, url: &str,
signature: &Signature, signature: &Signature,
config: Option<RpcSignatureSubscribeConfig>, config: Option<RpcSignatureSubscribeConfig>,
) -> Result< ) -> Result<SignatureSubscription, PubsubClientError> {
(
PubsubSignatureResponse,
Receiver<RpcResponse<RpcSignatureResult>>,
),
PubsubClientError,
> {
let url = Url::parse(url)?; let url = Url::parse(url)?;
let (socket, _response) = connect(url)?; let (socket, _response) = connect(url)?;
let (sender, receiver) = channel::<RpcResponse<RpcSignatureResult>>(); let (sender, receiver) = channel();
let socket = Arc::new(RwLock::new(socket)); let socket = Arc::new(RwLock::new(socket));
let socket_clone = socket.clone(); let socket_clone = socket.clone();
@ -227,7 +286,7 @@ impl PubsubClient {
let body = json!({ let body = json!({
"jsonrpc":"2.0", "jsonrpc":"2.0",
"id":1, "id":1,
"method":format!("{}Subscribe", SIGNATURE_OPERATION), "method":"signatureSubscribe",
"params":[ "params":[
signature.to_string(), signature.to_string(),
config config
@ -238,8 +297,7 @@ impl PubsubClient {
PubsubClientSubscription::<RpcResponse<RpcSignatureResult>>::send_subscribe( PubsubClientSubscription::<RpcResponse<RpcSignatureResult>>::send_subscribe(
&socket_clone, &socket_clone,
body, body,
) )?;
.unwrap();
let t_cleanup = std::thread::spawn(move || { let t_cleanup = std::thread::spawn(move || {
loop { loop {
@ -267,15 +325,14 @@ impl PubsubClient {
info!("websocket - exited receive loop"); info!("websocket - exited receive loop");
}); });
let result: PubsubClientSubscription<RpcResponse<RpcSignatureResult>> = let result = PubsubClientSubscription {
PubsubClientSubscription { message_type: PhantomData,
message_type: PhantomData, operation: "signature",
operation: SIGNATURE_OPERATION, socket,
socket, subscription_id,
subscription_id, t_cleanup: Some(t_cleanup),
t_cleanup: Some(t_cleanup), exit,
exit, };
};
Ok((result, receiver)) Ok((result, receiver))
} }

View File

@ -71,6 +71,20 @@ pub struct RpcProgramAccountsConfig {
pub account_config: RpcAccountInfoConfig, pub account_config: RpcAccountInfoConfig,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum RpcTransactionLogsFilter {
All,
AllWithVotes,
Mentions(Vec<String>), // base58-encoded list of addresses
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcTransactionLogsConfig {
pub commitment: Option<CommitmentConfig>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum RpcTokenAccountsFilter { pub enum RpcTokenAccountsFilter {

View File

@ -108,6 +108,14 @@ pub enum RpcSignatureResult {
ReceivedSignature(ReceivedSignatureResult), ReceivedSignature(ReceivedSignatureResult),
} }
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct RpcLogsResponse {
pub signature: String, // Signature as base58 string
pub err: Option<TransactionError>,
pub logs: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ProcessedSignatureResult { pub struct ProcessedSignatureResult {

View File

@ -6,8 +6,13 @@ use jsonrpc_derive::rpc;
use jsonrpc_pubsub::{typed::Subscriber, Session, SubscriptionId}; use jsonrpc_pubsub::{typed::Subscriber, Session, SubscriptionId};
use solana_account_decoder::UiAccount; use solana_account_decoder::UiAccount;
use solana_client::{ use solana_client::{
rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig, RpcSignatureSubscribeConfig}, rpc_config::{
rpc_response::{Response as RpcResponse, RpcKeyedAccount, RpcSignatureResult, SlotInfo}, RpcAccountInfoConfig, RpcProgramAccountsConfig, RpcSignatureSubscribeConfig,
RpcTransactionLogsConfig, RpcTransactionLogsFilter,
},
rpc_response::{
Response as RpcResponse, RpcKeyedAccount, RpcLogsResponse, RpcSignatureResult, SlotInfo,
},
}; };
#[cfg(test)] #[cfg(test)]
use solana_runtime::bank_forks::BankForks; use solana_runtime::bank_forks::BankForks;
@ -75,6 +80,24 @@ pub trait RpcSolPubSub {
fn program_unsubscribe(&self, meta: Option<Self::Metadata>, id: SubscriptionId) fn program_unsubscribe(&self, meta: Option<Self::Metadata>, id: SubscriptionId)
-> Result<bool>; -> Result<bool>;
// Get logs for all transactions that reference the specified address
#[pubsub(subscription = "logsNotification", subscribe, name = "logsSubscribe")]
fn logs_subscribe(
&self,
meta: Self::Metadata,
subscriber: Subscriber<RpcResponse<RpcLogsResponse>>,
filter: RpcTransactionLogsFilter,
config: RpcTransactionLogsConfig,
);
// Unsubscribe from logs notification subscription.
#[pubsub(
subscription = "logsNotification",
unsubscribe,
name = "logsUnsubscribe"
)]
fn logs_unsubscribe(&self, meta: Option<Self::Metadata>, id: SubscriptionId) -> Result<bool>;
// Get notification when signature is verified // Get notification when signature is verified
// Accepts signature parameter as base-58 encoded string // Accepts signature parameter as base-58 encoded string
#[pubsub( #[pubsub(
@ -241,6 +264,67 @@ impl RpcSolPubSub for RpcSolPubSubImpl {
} }
} }
fn logs_subscribe(
&self,
_meta: Self::Metadata,
subscriber: Subscriber<RpcResponse<RpcLogsResponse>>,
filter: RpcTransactionLogsFilter,
config: RpcTransactionLogsConfig,
) {
info!("logs_subscribe");
let (address, include_votes) = match filter {
RpcTransactionLogsFilter::All => (None, false),
RpcTransactionLogsFilter::AllWithVotes => (None, true),
RpcTransactionLogsFilter::Mentions(addresses) => {
match addresses.len() {
1 => match param::<Pubkey>(&addresses[0], "mentions") {
Ok(address) => (Some(address), false),
Err(e) => {
subscriber.reject(e).unwrap();
return;
}
},
_ => {
// Room is reserved in the API to support multiple addresses, but for now
// the implementation only supports one
subscriber
.reject(Error {
code: ErrorCode::InvalidParams,
message: "Invalid Request: Only 1 address supported".into(),
data: None,
})
.unwrap();
return;
}
}
}
};
let id = self.uid.fetch_add(1, atomic::Ordering::Relaxed);
let sub_id = SubscriptionId::Number(id as u64);
self.subscriptions.add_logs_subscription(
address,
include_votes,
config.commitment,
sub_id,
subscriber,
)
}
fn logs_unsubscribe(&self, _meta: Option<Self::Metadata>, id: SubscriptionId) -> Result<bool> {
info!("logs_unsubscribe: id={:?}", id);
if self.subscriptions.remove_logs_subscription(&id) {
Ok(true)
} else {
Err(Error {
code: ErrorCode::InvalidParams,
message: "Invalid Request: Subscription id does not exist".into(),
data: None,
})
}
}
fn signature_subscribe( fn signature_subscribe(
&self, &self,
_meta: Self::Metadata, _meta: Self::Metadata,

View File

@ -17,12 +17,14 @@ use solana_client::{
rpc_filter::RpcFilterType, rpc_filter::RpcFilterType,
rpc_response::{ rpc_response::{
ProcessedSignatureResult, ReceivedSignatureResult, Response, RpcKeyedAccount, ProcessedSignatureResult, ReceivedSignatureResult, Response, RpcKeyedAccount,
RpcResponseContext, RpcSignatureResult, SlotInfo, RpcLogsResponse, RpcResponseContext, RpcSignatureResult, SlotInfo,
}, },
}; };
use solana_measure::measure::Measure; use solana_measure::measure::Measure;
use solana_runtime::{ use solana_runtime::{
bank::Bank, bank::{
Bank, TransactionLogCollectorConfig, TransactionLogCollectorFilter, TransactionLogInfo,
},
bank_forks::BankForks, bank_forks::BankForks,
commitment::{BlockCommitmentCache, CommitmentSlots}, commitment::{BlockCommitmentCache, CommitmentSlots},
}; };
@ -35,16 +37,16 @@ use solana_sdk::{
transaction, transaction,
}; };
use solana_vote_program::vote_state::Vote; use solana_vote_program::vote_state::Vote;
use std::sync::{
atomic::{AtomicBool, Ordering},
mpsc::{Receiver, RecvTimeoutError, SendError, Sender},
};
use std::thread::{Builder, JoinHandle};
use std::time::Duration;
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
iter, iter,
sync::{
atomic::{AtomicBool, Ordering},
mpsc::{Receiver, RecvTimeoutError, SendError, Sender},
},
sync::{Arc, Mutex, RwLock}, sync::{Arc, Mutex, RwLock},
thread::{Builder, JoinHandle},
time::Duration,
}; };
// Stuck on tokio 0.1 until the jsonrpc-pubsub crate upgrades to tokio 0.2 // Stuck on tokio 0.1 until the jsonrpc-pubsub crate upgrades to tokio 0.2
@ -52,6 +54,28 @@ use tokio_01::runtime::{Builder as RuntimeBuilder, Runtime, TaskExecutor};
const RECEIVE_DELAY_MILLIS: u64 = 100; const RECEIVE_DELAY_MILLIS: u64 = 100;
trait BankGetTransactionLogsAdapter {
fn get_transaction_logs_adapter(
&self,
stuff: &(Option<Pubkey>, bool),
) -> Option<Vec<TransactionLogInfo>>;
}
impl BankGetTransactionLogsAdapter for Bank {
fn get_transaction_logs_adapter(
&self,
config: &(Option<Pubkey>, bool),
) -> Option<Vec<TransactionLogInfo>> {
let mut logs = self.get_transaction_logs(config.0.as_ref());
if config.0.is_none() && !config.1 {
// Filter out votes if the subscriber doesn't want them
logs = logs.map(|logs| logs.into_iter().filter(|log| !log.is_vote).collect());
}
logs
}
}
// A more human-friendly version of Vote, with the bank state signature base58 encoded. // A more human-friendly version of Vote, with the bank state signature base58 encoded.
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct RpcVote { pub struct RpcVote {
@ -103,6 +127,12 @@ type RpcAccountSubscriptions = RwLock<
HashMap<SubscriptionId, SubscriptionData<Response<UiAccount>, UiAccountEncoding>>, HashMap<SubscriptionId, SubscriptionData<Response<UiAccount>, UiAccountEncoding>>,
>, >,
>; >;
type RpcLogsSubscriptions = RwLock<
HashMap<
(Option<Pubkey>, bool),
HashMap<SubscriptionId, SubscriptionData<Response<RpcLogsResponse>, ()>>,
>,
>;
type RpcProgramSubscriptions = RwLock< type RpcProgramSubscriptions = RwLock<
HashMap< HashMap<
Pubkey, Pubkey,
@ -182,7 +212,7 @@ where
S: Clone + Serialize, S: Clone + Serialize,
B: Fn(&Bank, &K) -> X, B: Fn(&Bank, &K) -> X,
F: Fn(X, &K, Slot, Option<T>, Arc<Bank>) -> (Box<dyn Iterator<Item = S>>, Slot), F: Fn(X, &K, Slot, Option<T>, Arc<Bank>) -> (Box<dyn Iterator<Item = S>>, Slot),
X: Clone + Serialize + Default, X: Clone + Default,
T: Clone, T: Clone,
{ {
let mut notified_set: HashSet<SubscriptionId> = HashSet::new(); let mut notified_set: HashSet<SubscriptionId> = HashSet::new();
@ -321,12 +351,34 @@ fn filter_program_results(
(accounts, last_notified_slot) (accounts, last_notified_slot)
} }
fn filter_logs_results(
logs: Option<Vec<TransactionLogInfo>>,
_address: &(Option<Pubkey>, bool),
last_notified_slot: Slot,
_config: Option<()>,
_bank: Arc<Bank>,
) -> (Box<dyn Iterator<Item = RpcLogsResponse>>, Slot) {
match logs {
None => (Box::new(iter::empty()), last_notified_slot),
Some(logs) => (
Box::new(logs.into_iter().map(|log| RpcLogsResponse {
signature: log.signature.to_string(),
err: log.result.err(),
logs: log.log_messages,
})),
last_notified_slot,
),
}
}
#[derive(Clone)] #[derive(Clone)]
struct Subscriptions { struct Subscriptions {
account_subscriptions: Arc<RpcAccountSubscriptions>, account_subscriptions: Arc<RpcAccountSubscriptions>,
program_subscriptions: Arc<RpcProgramSubscriptions>, program_subscriptions: Arc<RpcProgramSubscriptions>,
logs_subscriptions: Arc<RpcLogsSubscriptions>,
signature_subscriptions: Arc<RpcSignatureSubscriptions>, signature_subscriptions: Arc<RpcSignatureSubscriptions>,
gossip_account_subscriptions: Arc<RpcAccountSubscriptions>, gossip_account_subscriptions: Arc<RpcAccountSubscriptions>,
gossip_logs_subscriptions: Arc<RpcLogsSubscriptions>,
gossip_program_subscriptions: Arc<RpcProgramSubscriptions>, gossip_program_subscriptions: Arc<RpcProgramSubscriptions>,
gossip_signature_subscriptions: Arc<RpcSignatureSubscriptions>, gossip_signature_subscriptions: Arc<RpcSignatureSubscriptions>,
slot_subscriptions: Arc<RpcSlotSubscriptions>, slot_subscriptions: Arc<RpcSlotSubscriptions>,
@ -383,9 +435,11 @@ impl RpcSubscriptions {
) = std::sync::mpsc::channel(); ) = std::sync::mpsc::channel();
let account_subscriptions = Arc::new(RpcAccountSubscriptions::default()); let account_subscriptions = Arc::new(RpcAccountSubscriptions::default());
let logs_subscriptions = Arc::new(RpcLogsSubscriptions::default());
let program_subscriptions = Arc::new(RpcProgramSubscriptions::default()); let program_subscriptions = Arc::new(RpcProgramSubscriptions::default());
let signature_subscriptions = Arc::new(RpcSignatureSubscriptions::default()); let signature_subscriptions = Arc::new(RpcSignatureSubscriptions::default());
let gossip_account_subscriptions = Arc::new(RpcAccountSubscriptions::default()); let gossip_account_subscriptions = Arc::new(RpcAccountSubscriptions::default());
let gossip_logs_subscriptions = Arc::new(RpcLogsSubscriptions::default());
let gossip_program_subscriptions = Arc::new(RpcProgramSubscriptions::default()); let gossip_program_subscriptions = Arc::new(RpcProgramSubscriptions::default());
let gossip_signature_subscriptions = Arc::new(RpcSignatureSubscriptions::default()); let gossip_signature_subscriptions = Arc::new(RpcSignatureSubscriptions::default());
let slot_subscriptions = Arc::new(RpcSlotSubscriptions::default()); let slot_subscriptions = Arc::new(RpcSlotSubscriptions::default());
@ -398,9 +452,11 @@ impl RpcSubscriptions {
let exit_clone = exit.clone(); let exit_clone = exit.clone();
let subscriptions = Subscriptions { let subscriptions = Subscriptions {
account_subscriptions, account_subscriptions,
logs_subscriptions,
program_subscriptions, program_subscriptions,
signature_subscriptions, signature_subscriptions,
gossip_account_subscriptions, gossip_account_subscriptions,
gossip_logs_subscriptions,
gossip_program_subscriptions, gossip_program_subscriptions,
gossip_signature_subscriptions, gossip_signature_subscriptions,
slot_subscriptions, slot_subscriptions,
@ -474,6 +530,25 @@ impl RpcSubscriptions {
) )
} }
fn check_logs(
address_with_enable_votes_flag: &(Option<Pubkey>, bool),
bank_forks: &Arc<RwLock<BankForks>>,
logs_subscriptions: Arc<RpcLogsSubscriptions>,
notifier: &RpcNotifier,
commitment_slots: &CommitmentSlots,
) -> HashSet<SubscriptionId> {
let subscriptions = logs_subscriptions.read().unwrap();
check_commitment_and_notify(
&subscriptions,
address_with_enable_votes_flag,
bank_forks,
commitment_slots,
Bank::get_transaction_logs_adapter,
filter_logs_results,
notifier,
)
}
fn check_program( fn check_program(
program_id: &Pubkey, program_id: &Pubkey,
bank_forks: &Arc<RwLock<BankForks>>, bank_forks: &Arc<RwLock<BankForks>>,
@ -647,6 +722,114 @@ impl RpcSubscriptions {
} }
} }
pub fn add_logs_subscription(
&self,
address: Option<Pubkey>,
include_votes: bool,
commitment: Option<CommitmentConfig>,
sub_id: SubscriptionId,
subscriber: Subscriber<Response<RpcLogsResponse>>,
) {
let commitment = commitment.unwrap_or_else(CommitmentConfig::single_gossip);
{
let mut subscriptions = if commitment.commitment == CommitmentLevel::SingleGossip {
self.subscriptions
.gossip_logs_subscriptions
.write()
.unwrap()
} else {
self.subscriptions.logs_subscriptions.write().unwrap()
};
add_subscription(
&mut subscriptions,
(address, include_votes),
commitment,
sub_id,
subscriber,
0, // last_notified_slot is not utilized for logs subscriptions
None,
);
}
self.update_bank_transaction_log_keys();
}
pub fn remove_logs_subscription(&self, id: &SubscriptionId) -> bool {
let mut removed = {
let mut subscriptions = self.subscriptions.logs_subscriptions.write().unwrap();
remove_subscription(&mut subscriptions, id)
};
if !removed {
removed = {
let mut subscriptions = self
.subscriptions
.gossip_logs_subscriptions
.write()
.unwrap();
remove_subscription(&mut subscriptions, id)
};
}
if removed {
self.update_bank_transaction_log_keys();
}
removed
}
fn update_bank_transaction_log_keys(&self) {
// Grab a write lock for both `logs_subscriptions` and `gossip_logs_subscriptions`, to
// ensure `Bank::transaction_log_collector_config` is updated atomically.
let logs_subscriptions = self.subscriptions.logs_subscriptions.write().unwrap();
let gossip_logs_subscriptions = self
.subscriptions
.gossip_logs_subscriptions
.write()
.unwrap();
let mut config = TransactionLogCollectorConfig::default();
let mut all = false;
let mut all_with_votes = false;
let mut mentioned_address = false;
for (address, with_votes) in logs_subscriptions
.keys()
.chain(gossip_logs_subscriptions.keys())
{
match address {
None => {
if *with_votes {
all_with_votes = true;
} else {
all = true;
}
}
Some(address) => {
config.mentioned_addresses.insert(*address);
mentioned_address = true;
}
}
}
config.filter = if all_with_votes {
TransactionLogCollectorFilter::AllWithVotes
} else if all {
TransactionLogCollectorFilter::All
} else if mentioned_address {
TransactionLogCollectorFilter::OnlyMentionedAddresses
} else {
TransactionLogCollectorFilter::None
};
*self
.bank_forks
.read()
.unwrap()
.root_bank()
.transaction_log_collector_config
.write()
.unwrap() = config;
}
pub fn add_signature_subscription( pub fn add_signature_subscription(
&self, &self,
signature: Signature, signature: Signature,
@ -847,8 +1030,9 @@ impl RpcSubscriptions {
} }
} }
NotificationEntry::Bank(commitment_slots) => { NotificationEntry::Bank(commitment_slots) => {
RpcSubscriptions::notify_accounts_programs_signatures( RpcSubscriptions::notify_accounts_logs_programs_signatures(
&subscriptions.account_subscriptions, &subscriptions.account_subscriptions,
&subscriptions.logs_subscriptions,
&subscriptions.program_subscriptions, &subscriptions.program_subscriptions,
&subscriptions.signature_subscriptions, &subscriptions.signature_subscriptions,
&bank_forks, &bank_forks,
@ -894,8 +1078,9 @@ impl RpcSubscriptions {
highest_confirmed_slot: slot, highest_confirmed_slot: slot,
..CommitmentSlots::default() ..CommitmentSlots::default()
}; };
RpcSubscriptions::notify_accounts_programs_signatures( RpcSubscriptions::notify_accounts_logs_programs_signatures(
&subscriptions.gossip_account_subscriptions, &subscriptions.gossip_account_subscriptions,
&subscriptions.gossip_logs_subscriptions,
&subscriptions.gossip_program_subscriptions, &subscriptions.gossip_program_subscriptions,
&subscriptions.gossip_signature_subscriptions, &subscriptions.gossip_signature_subscriptions,
bank_forks, bank_forks,
@ -905,8 +1090,9 @@ impl RpcSubscriptions {
); );
} }
fn notify_accounts_programs_signatures( fn notify_accounts_logs_programs_signatures(
account_subscriptions: &Arc<RpcAccountSubscriptions>, account_subscriptions: &Arc<RpcAccountSubscriptions>,
logs_subscriptions: &Arc<RpcLogsSubscriptions>,
program_subscriptions: &Arc<RpcProgramSubscriptions>, program_subscriptions: &Arc<RpcProgramSubscriptions>,
signature_subscriptions: &Arc<RpcSignatureSubscriptions>, signature_subscriptions: &Arc<RpcSignatureSubscriptions>,
bank_forks: &Arc<RwLock<BankForks>>, bank_forks: &Arc<RwLock<BankForks>>,
@ -932,6 +1118,24 @@ impl RpcSubscriptions {
} }
accounts_time.stop(); accounts_time.stop();
let mut logs_time = Measure::start("logs");
let logs: Vec<_> = {
let subs = logs_subscriptions.read().unwrap();
subs.keys().cloned().collect()
};
let mut num_logs_notified = 0;
for address in &logs {
num_logs_notified += Self::check_logs(
address,
bank_forks,
logs_subscriptions.clone(),
&notifier,
&commitment_slots,
)
.len();
}
logs_time.stop();
let mut programs_time = Measure::start("programs"); let mut programs_time = Measure::start("programs");
let programs: Vec<_> = { let programs: Vec<_> = {
let subs = program_subscriptions.read().unwrap(); let subs = program_subscriptions.read().unwrap();
@ -990,6 +1194,9 @@ impl RpcSubscriptions {
("num_account_subscriptions", pubkeys.len(), i64), ("num_account_subscriptions", pubkeys.len(), i64),
("num_account_pubkeys_notified", num_pubkeys_notified, i64), ("num_account_pubkeys_notified", num_pubkeys_notified, i64),
("accounts_time", accounts_time.as_us() as i64, i64), ("accounts_time", accounts_time.as_us() as i64, i64),
("num_logs_subscriptions", logs.len(), i64),
("num_logs_notified", num_logs_notified, i64),
("logs_time", logs_time.as_us() as i64, i64),
("num_program_subscriptions", programs.len(), i64), ("num_program_subscriptions", programs.len(), i64),
("num_programs_notified", num_programs_notified, i64), ("num_programs_notified", num_programs_notified, i64),
("programs_time", programs_time.as_us() as i64, i64), ("programs_time", programs_time.as_us() as i64, i64),

View File

@ -63,6 +63,8 @@ gives a convenient interface for the RPC methods.
- [Subscription Websocket](jsonrpc-api.md#subscription-websocket) - [Subscription Websocket](jsonrpc-api.md#subscription-websocket)
- [accountSubscribe](jsonrpc-api.md#accountsubscribe) - [accountSubscribe](jsonrpc-api.md#accountsubscribe)
- [accountUnsubscribe](jsonrpc-api.md#accountunsubscribe) - [accountUnsubscribe](jsonrpc-api.md#accountunsubscribe)
- [logsSubscribe](jsonrpc-api.md#logssubscribe)
- [logsUnsubscribe](jsonrpc-api.md#logsunsubscribe)
- [programSubscribe](jsonrpc-api.md#programsubscribe) - [programSubscribe](jsonrpc-api.md#programsubscribe)
- [programUnsubscribe](jsonrpc-api.md#programunsubscribe) - [programUnsubscribe](jsonrpc-api.md#programunsubscribe)
- [signatureSubscribe](jsonrpc-api.md#signaturesubscribe) - [signatureSubscribe](jsonrpc-api.md#signaturesubscribe)
@ -2876,7 +2878,7 @@ Request:
"CM78CPUeXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNH12", "CM78CPUeXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNH12",
{ {
"encoding": "base64", "encoding": "base64",
"commitment": "single" "commitment": "root"
} }
] ]
} }
@ -2983,6 +2985,103 @@ Result:
{"jsonrpc": "2.0","result": true,"id": 1} {"jsonrpc": "2.0","result": true,"id": 1}
``` ```
### logsSubscribe
Subscribe to transaction logging. **UNSTABLE**
#### Parameters:
- `filter: <string>|<object>` - filter criteria for the logs to receive results by account type; currently supported:
- "all" - subscribe to all transactions except for simple vote transactions
- "allWithVotes" - subscribe to all transactions including simple vote transactions
- `{ "mentions": [ <string> ] }` - subscribe to all transactions that mention the provided Pubkey (as base-58 encoded string)
- `<object>` - (optional) Configuration object containing the following optional fields:
- (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment)
#### Results:
- `<integer>` - Subscription id \(needed to unsubscribe\)
#### Example:
Request:
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "logsSubscribe",
"params": [
{
"mentions": [ "11111111111111111111111111111111" ]
}
{
"commitment": "max"
}
]
}
{
"jsonrpc": "2.0",
"id": 1,
"method": "logsSubscribe",
"params": [ "all" ]
}
```
Result:
```json
{"jsonrpc": "2.0","result": 24040,"id": 1}
```
#### Notification Format:
Base58 encoding:
```json
{
"jsonrpc": "2.0",
"method": "logsNotification",
"params": {
"result": {
"context": {
"slot": 5208469
},
"value": {
"signature": "5h6xBEauJ3PK6SWCZ1PGjBvj8vDdWG3KpwATGy1ARAXFSDwt8GFXM7W5Ncn16wmqokgpiKRLuS83KUxyZyv2sUYv",
"err": null,
"logs": [
"BPF program 83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri success"
]
}
},
"subscription": 24040
}
}
```
### logsUnsubscribe
Unsubscribe from transaction logging
#### Parameters:
- `<integer>` - id of subscription to cancel
#### Results:
- `<bool>` - unsubscribe success message
#### Example:
Request:
```json
{"jsonrpc":"2.0", "id":1, "method":"logsUnsubscribe", "params":[0]}
```
Result:
```json
{"jsonrpc": "2.0","result": true,"id": 1}
```
### programSubscribe ### programSubscribe
Subscribe to a program to receive notifications when the lamports or data for a given account owned by the program changes Subscribe to a program to receive notifications when the lamports or data for a given account owned by the program changes
@ -3012,7 +3111,7 @@ Request:
"11111111111111111111111111111111", "11111111111111111111111111111111",
{ {
"encoding": "base64", "encoding": "base64",
"commitment": "single" "commitment": "max"
} }
] ]
} }

View File

@ -404,6 +404,45 @@ pub type InnerInstructionsList = Vec<InnerInstructions>;
/// A list of log messages emitted during a transaction /// A list of log messages emitted during a transaction
pub type TransactionLogMessages = Vec<String>; pub type TransactionLogMessages = Vec<String>;
#[derive(Serialize, Deserialize, AbiExample, AbiEnumVisitor, Debug, PartialEq)]
pub enum TransactionLogCollectorFilter {
All,
AllWithVotes,
None,
OnlyMentionedAddresses,
}
impl Default for TransactionLogCollectorFilter {
fn default() -> Self {
Self::None
}
}
#[derive(AbiExample, Debug, Default)]
pub struct TransactionLogCollectorConfig {
pub mentioned_addresses: HashSet<Pubkey>,
pub filter: TransactionLogCollectorFilter,
}
#[derive(AbiExample, Clone, Debug)]
pub struct TransactionLogInfo {
pub signature: Signature,
pub result: Result<()>,
pub is_vote: bool,
pub log_messages: TransactionLogMessages,
}
#[derive(AbiExample, Default, Debug)]
pub struct TransactionLogCollector {
// All the logs collected for from this Bank. Exact contents depend on the
// active `TransactionLogCollectorFilter`
pub logs: Vec<TransactionLogInfo>,
// For each `mentioned_addresses`, maintain a list of indicies into `logs` to easily
// locate the logs from transactions that included the mentioned addresses.
pub mentioned_address_map: HashMap<Pubkey, Vec<usize>>,
}
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub enum HashAgeKind { pub enum HashAgeKind {
Extant, Extant,
@ -724,6 +763,13 @@ pub struct Bank {
transaction_debug_keys: Option<Arc<HashSet<Pubkey>>>, transaction_debug_keys: Option<Arc<HashSet<Pubkey>>>,
// Global configuration for how transaction logs should be collected across all banks
pub transaction_log_collector_config: Arc<RwLock<TransactionLogCollectorConfig>>,
// Logs from transactions that this Bank executed collected according to the criteria in
// `transaction_log_collector_config`
pub transaction_log_collector: Arc<RwLock<TransactionLogCollector>>,
pub feature_set: Arc<FeatureSet>, pub feature_set: Arc<FeatureSet>,
} }
@ -868,6 +914,8 @@ impl Bank {
rewards_pool_pubkeys: parent.rewards_pool_pubkeys.clone(), rewards_pool_pubkeys: parent.rewards_pool_pubkeys.clone(),
cached_executors: RwLock::new((*parent.cached_executors.read().unwrap()).clone()), cached_executors: RwLock::new((*parent.cached_executors.read().unwrap()).clone()),
transaction_debug_keys: parent.transaction_debug_keys.clone(), transaction_debug_keys: parent.transaction_debug_keys.clone(),
transaction_log_collector_config: parent.transaction_log_collector_config.clone(),
transaction_log_collector: Arc::new(RwLock::new(TransactionLogCollector::default())),
feature_set: parent.feature_set.clone(), feature_set: parent.feature_set.clone(),
}; };
@ -984,6 +1032,8 @@ impl Bank {
CachedExecutors::new(MAX_CACHED_EXECUTORS), CachedExecutors::new(MAX_CACHED_EXECUTORS),
)))), )))),
transaction_debug_keys: debug_keys, transaction_debug_keys: debug_keys,
transaction_log_collector_config: new(),
transaction_log_collector: new(),
feature_set: new(), feature_set: new(),
}; };
bank.finish_init(genesis_config, additional_builtins); bank.finish_init(genesis_config, additional_builtins);
@ -2117,7 +2167,10 @@ impl Bank {
} }
/// Run transactions against a frozen bank without committing the results /// Run transactions against a frozen bank without committing the results
pub fn simulate_transaction(&self, transaction: Transaction) -> (Result<()>, Vec<String>) { pub fn simulate_transaction(
&self,
transaction: Transaction,
) -> (Result<()>, TransactionLogMessages) {
assert!(self.is_frozen(), "simulation bank must be frozen"); assert!(self.is_frozen(), "simulation bank must be frozen");
let txs = &[transaction]; let txs = &[transaction];
@ -2127,7 +2180,7 @@ impl Bank {
_loaded_accounts, _loaded_accounts,
executed, executed,
_inner_instructions, _inner_instructions,
transaction_logs, log_messages,
_retryable_transactions, _retryable_transactions,
_transaction_count, _transaction_count,
_signature_count, _signature_count,
@ -2142,7 +2195,7 @@ impl Bank {
); );
let transaction_result = executed[0].0.clone().map(|_| ()); let transaction_result = executed[0].0.clone().map(|_| ());
let log_messages = transaction_logs let log_messages = log_messages
.get(0) .get(0)
.map_or(vec![], |messages| messages.to_vec()); .map_or(vec![], |messages| messages.to_vec());
@ -2182,6 +2235,7 @@ impl Bank {
&self.feature_set, &self.feature_set,
) )
} }
fn check_age( fn check_age(
&self, &self,
txs: &[Transaction], txs: &[Transaction],
@ -2213,6 +2267,7 @@ impl Bank {
}) })
.collect() .collect()
} }
fn check_signatures( fn check_signatures(
&self, &self,
txs: &[Transaction], txs: &[Transaction],
@ -2249,6 +2304,7 @@ impl Bank {
}) })
.collect() .collect()
} }
fn filter_by_vote_transactions( fn filter_by_vote_transactions(
&self, &self,
txs: &[Transaction], txs: &[Transaction],
@ -2260,23 +2316,8 @@ impl Bank {
.zip(lock_results.into_iter()) .zip(lock_results.into_iter())
.map(|((_, tx), lock_res)| { .map(|((_, tx), lock_res)| {
if lock_res.0.is_ok() { if lock_res.0.is_ok() {
if tx.message.instructions.len() == 1 { if is_simple_vote_transaction(tx) {
let instruction = &tx.message.instructions[0]; return lock_res;
let program_pubkey =
tx.message.account_keys[instruction.program_id_index as usize];
if program_pubkey == solana_vote_program::id() {
if let Ok(vote_instruction) =
limited_deserialize::<VoteInstruction>(&instruction.data)
{
match vote_instruction {
VoteInstruction::Vote(_)
| VoteInstruction::VoteSwitch(_, _) => {
return lock_res;
}
_ => {}
}
}
}
} }
error_counters.not_allowed_during_cluster_maintenance += 1; error_counters.not_allowed_during_cluster_maintenance += 1;
@ -2598,7 +2639,7 @@ impl Bank {
let mut signature_count: u64 = 0; let mut signature_count: u64 = 0;
let mut inner_instructions: Vec<Option<InnerInstructionsList>> = let mut inner_instructions: Vec<Option<InnerInstructionsList>> =
Vec::with_capacity(txs.len()); Vec::with_capacity(txs.len());
let mut transaction_logs: Vec<TransactionLogMessages> = Vec::with_capacity(txs.len()); let mut transaction_log_messages = Vec::with_capacity(txs.len());
let bpf_compute_budget = self let bpf_compute_budget = self
.bpf_compute_budget .bpf_compute_budget
.unwrap_or_else(|| BpfComputeBudget::new(&self.feature_set)); .unwrap_or_else(|| BpfComputeBudget::new(&self.feature_set));
@ -2649,7 +2690,7 @@ impl Bank {
.unwrap_or_default() .unwrap_or_default()
.into(); .into();
transaction_logs.push(log_messages); transaction_log_messages.push(log_messages);
} }
Self::compile_recorded_instructions( Self::compile_recorded_instructions(
@ -2686,7 +2727,10 @@ impl Bank {
let mut tx_count: u64 = 0; let mut tx_count: u64 = 0;
let err_count = &mut error_counters.total; let err_count = &mut error_counters.total;
for ((r, _hash_age_kind), tx) in executed.iter().zip(txs.iter()) { let transaction_log_collector_config =
self.transaction_log_collector_config.read().unwrap();
for (i, ((r, _hash_age_kind), tx)) in executed.iter().zip(txs.iter()).enumerate() {
if let Some(debug_keys) = &self.transaction_debug_keys { if let Some(debug_keys) = &self.transaction_debug_keys {
for key in &tx.message.account_keys { for key in &tx.message.account_keys {
if debug_keys.contains(key) { if debug_keys.contains(key) {
@ -2695,6 +2739,50 @@ impl Bank {
} }
} }
} }
if transaction_log_collector_config.filter != TransactionLogCollectorFilter::None {
let mut transaction_log_collector = self.transaction_log_collector.write().unwrap();
let transaction_log_index = transaction_log_collector.logs.len();
let mut mentioned_address = false;
if !transaction_log_collector_config
.mentioned_addresses
.is_empty()
{
for key in &tx.message.account_keys {
if transaction_log_collector_config
.mentioned_addresses
.contains(key)
{
transaction_log_collector
.mentioned_address_map
.entry(*key)
.or_default()
.push(transaction_log_index);
mentioned_address = true;
}
}
}
let is_vote = is_simple_vote_transaction(tx);
let store = match transaction_log_collector_config.filter {
TransactionLogCollectorFilter::All => !is_vote || mentioned_address,
TransactionLogCollectorFilter::AllWithVotes => true,
TransactionLogCollectorFilter::None => false,
TransactionLogCollectorFilter::OnlyMentionedAddresses => mentioned_address,
};
if store {
transaction_log_collector.logs.push(TransactionLogInfo {
signature: tx.signatures[0],
result: r.clone(),
is_vote,
log_messages: transaction_log_messages.get(i).cloned().unwrap_or_default(),
});
}
}
if r.is_ok() { if r.is_ok() {
tx_count += 1; tx_count += 1;
} else { } else {
@ -2716,7 +2804,7 @@ impl Bank {
loaded_accounts, loaded_accounts,
executed, executed,
inner_instructions, inner_instructions,
transaction_logs, transaction_log_messages,
retryable_txs, retryable_txs,
tx_count, tx_count,
signature_count, signature_count,
@ -3673,6 +3761,26 @@ impl Bank {
.load_by_program_slot(self.slot(), Some(program_id)) .load_by_program_slot(self.slot(), Some(program_id))
} }
pub fn get_transaction_logs(
&self,
address: Option<&Pubkey>,
) -> Option<Vec<TransactionLogInfo>> {
let transaction_log_collector = self.transaction_log_collector.read().unwrap();
match address {
None => Some(transaction_log_collector.logs.clone()),
Some(address) => transaction_log_collector
.mentioned_address_map
.get(address)
.map(|log_indices| {
log_indices
.iter()
.map(|i| transaction_log_collector.logs[*i].clone())
.collect()
}),
}
}
pub fn get_all_accounts_modified_since_parent(&self) -> Vec<(Pubkey, Account)> { pub fn get_all_accounts_modified_since_parent(&self) -> Vec<(Pubkey, Account)> {
self.rc.accounts.load_by_program_slot(self.slot(), None) self.rc.accounts.load_by_program_slot(self.slot(), None)
} }
@ -4377,6 +4485,21 @@ pub fn goto_end_of_slot(bank: &mut Bank) {
} }
} }
fn is_simple_vote_transaction(transaction: &Transaction) -> bool {
if transaction.message.instructions.len() == 1 {
let instruction = &transaction.message.instructions[0];
let program_pubkey =
transaction.message.account_keys[instruction.program_id_index as usize];
if program_pubkey == solana_vote_program::id() {
if let Ok(vote_instruction) = limited_deserialize::<VoteInstruction>(&instruction.data)
{
return matches!(vote_instruction, VoteInstruction::Vote(_) | VoteInstruction::VoteSwitch(_, _));
}
}
}
false
}
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use super::*; use super::*;