From a17d5795fba576c99c69ce67735b4e31957e961c Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Sun, 12 Jan 2020 22:34:30 -0700 Subject: [PATCH] getConfirmedBlock: add encoding optional parameter (#7756) automerge --- book/src/api-reference/jsonrpc-api.md | 7 ++- client/src/rpc_request.rs | 90 +++++++++++++++++++++++++-- core/src/banking_stage.rs | 29 +++++---- core/src/replay_stage.rs | 29 +++++---- core/src/rpc.rs | 77 ++++++++++++++++++----- ledger/src/blocktree.rs | 86 ++++++++++++++++++------- 6 files changed, 244 insertions(+), 74 deletions(-) diff --git a/book/src/api-reference/jsonrpc-api.md b/book/src/api-reference/jsonrpc-api.md index b28f8d33dc..740c693493 100644 --- a/book/src/api-reference/jsonrpc-api.md +++ b/book/src/api-reference/jsonrpc-api.md @@ -278,16 +278,17 @@ Returns identity and transaction information about a confirmed block in the ledg #### Parameters: * `integer` - slot, as u64 integer +* `string` - (optional) encoding for each returned Transaction, either "json" or "binary". If not provided, the default encoding is JSON. #### Results: The result field will be an object with the following fields: -* `blockhash` - the blockhash of this block -* `previousBlockhash` - the blockhash of this block's parent +* `blockhash` - the blockhash of this block, as base-58 encoded string +* `previousBlockhash` - the blockhash of this block's parent, as base-58 encoded string * `parentSlot` - the slot index of this block's parent * `transactions` - an array of tuples containing: - * [Transaction](transaction-api.md) object, in JSON format + * [Transaction](transaction-api.md) object, either in JSON format or base-58 encoded binary data, depending on encoding parameter * Transaction status object, containing: * `status` - Transaction status: * `"Ok": null` - Transaction was successful diff --git a/client/src/rpc_request.rs b/client/src/rpc_request.rs index 4bd370132b..2c01661b42 100644 --- a/client/src/rpc_request.rs +++ b/client/src/rpc_request.rs @@ -1,8 +1,9 @@ +use bincode::serialize; use jsonrpc_core::Result as JsonResult; use serde_json::{json, Value}; use solana_sdk::{ clock::{Epoch, Slot}, - hash::Hash, + message::MessageHeader, transaction::{Result, Transaction}, }; use std::{collections::HashMap, error, fmt, io, net::SocketAddr}; @@ -21,13 +22,92 @@ pub struct Response { pub value: T, } -#[derive(Debug, Default, PartialEq, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcConfirmedBlock { - pub previous_blockhash: Hash, - pub blockhash: Hash, + pub previous_blockhash: String, + pub blockhash: String, pub parent_slot: Slot, - pub transactions: Vec<(Transaction, Option)>, + pub transactions: Vec<(RpcEncodedTransaction, Option)>, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum RpcTransactionEncoding { + Binary, + Json, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", untagged)] +pub enum RpcEncodedTransaction { + Binary(String), + Json(RpcTransaction), +} + +impl RpcEncodedTransaction { + pub fn encode(transaction: Transaction, encoding: RpcTransactionEncoding) -> Self { + if encoding == RpcTransactionEncoding::Json { + RpcEncodedTransaction::Json(RpcTransaction { + signatures: transaction + .signatures + .iter() + .map(|sig| sig.to_string()) + .collect(), + message: RpcMessage { + header: transaction.message.header, + account_keys: transaction + .message + .account_keys + .iter() + .map(|pubkey| pubkey.to_string()) + .collect(), + recent_blockhash: transaction.message.recent_blockhash.to_string(), + instructions: transaction + .message + .instructions + .iter() + .map(|instruction| RpcCompiledInstruction { + program_id_index: instruction.program_id_index, + accounts: instruction.accounts.clone(), + data: bs58::encode(instruction.data.clone()).into_string(), + }) + .collect(), + }, + }) + } else { + RpcEncodedTransaction::Binary( + bs58::encode(serialize(&transaction).unwrap()).into_string(), + ) + } + } +} + +/// A duplicate representation of a Transaction for pretty JSON serialization +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcTransaction { + pub signatures: Vec, + pub message: RpcMessage, +} + +/// A duplicate representation of a Message for pretty JSON serialization +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcMessage { + pub header: MessageHeader, + pub account_keys: Vec, + pub recent_blockhash: String, + pub instructions: Vec, +} + +/// A duplicate representation of a Message for pretty JSON serialization +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcCompiledInstruction { + pub program_id_index: u8, + pub accounts: Vec, + pub data: String, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index 975b8ab86a..d15b5741f6 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -1020,6 +1020,7 @@ mod tests { }; use crossbeam_channel::unbounded; use itertools::Itertools; + use solana_client::rpc_request::RpcEncodedTransaction; use solana_ledger::{ blocktree::entries_to_test_shreds, entry::{next_entry, Entry, EntrySlice}, @@ -1971,22 +1972,24 @@ mod tests { transaction_status_service.join().unwrap(); - let confirmed_block = blocktree.get_confirmed_block(bank.slot()).unwrap(); + let confirmed_block = blocktree.get_confirmed_block(bank.slot(), None).unwrap(); assert_eq!(confirmed_block.transactions.len(), 3); for (transaction, result) in confirmed_block.transactions.into_iter() { - if transaction.signatures[0] == success_signature { - assert_eq!(result.unwrap().status, Ok(())); - } else if transaction.signatures[0] == ix_error_signature { - assert_eq!( - result.unwrap().status, - Err(TransactionError::InstructionError( - 0, - InstructionError::CustomError(1) - )) - ); - } else { - assert_eq!(result, None); + if let RpcEncodedTransaction::Json(transaction) = transaction { + if transaction.signatures[0] == success_signature.to_string() { + assert_eq!(result.unwrap().status, Ok(())); + } else if transaction.signatures[0] == ix_error_signature.to_string() { + assert_eq!( + result.unwrap().status, + Err(TransactionError::InstructionError( + 0, + InstructionError::CustomError(1) + )) + ); + } else { + assert_eq!(result, None); + } } } } diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index e7ef0ef553..a8831e2ae8 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -1186,6 +1186,7 @@ pub(crate) mod tests { transaction_status_service::TransactionStatusService, }; use crossbeam_channel::unbounded; + use solana_client::rpc_request::RpcEncodedTransaction; use solana_ledger::{ blocktree::make_slot_entries, blocktree::{entries_to_test_shreds, BlocktreeError}, @@ -1988,22 +1989,24 @@ pub(crate) mod tests { blocktree.clone(), ); - let confirmed_block = blocktree.get_confirmed_block(slot).unwrap(); + let confirmed_block = blocktree.get_confirmed_block(slot, None).unwrap(); assert_eq!(confirmed_block.transactions.len(), 3); for (transaction, result) in confirmed_block.transactions.into_iter() { - if transaction.signatures[0] == signatures[0] { - assert_eq!(result.unwrap().status, Ok(())); - } else if transaction.signatures[0] == signatures[1] { - assert_eq!( - result.unwrap().status, - Err(TransactionError::InstructionError( - 0, - InstructionError::CustomError(1) - )) - ); - } else { - assert_eq!(result, None); + if let RpcEncodedTransaction::Json(transaction) = transaction { + if transaction.signatures[0] == signatures[0].to_string() { + assert_eq!(result.unwrap().status, Ok(())); + } else if transaction.signatures[0] == signatures[1].to_string() { + assert_eq!( + result.unwrap().status, + Err(TransactionError::InstructionError( + 0, + InstructionError::CustomError(1) + )) + ); + } else { + assert_eq!(result, None); + } } } } diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 053a6088e0..5c9099b04b 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -13,7 +13,8 @@ use jsonrpc_core::{Error, Metadata, Result}; use jsonrpc_derive::rpc; use solana_client::rpc_request::{ Response, RpcConfirmedBlock, RpcContactInfo, RpcEpochInfo, RpcLeaderSchedule, - RpcResponseContext, RpcVersionInfo, RpcVoteAccountInfo, RpcVoteAccountStatus, + RpcResponseContext, RpcTransactionEncoding, RpcVersionInfo, RpcVoteAccountInfo, + RpcVoteAccountStatus, }; use solana_faucet::faucet::request_airdrop_transaction; use solana_ledger::{ @@ -312,8 +313,12 @@ impl JsonRpcRequestProcessor { } } - pub fn get_confirmed_block(&self, slot: Slot) -> Result> { - Ok(self.blocktree.get_confirmed_block(slot).ok()) + pub fn get_confirmed_block( + &self, + slot: Slot, + encoding: Option, + ) -> Result> { + Ok(self.blocktree.get_confirmed_block(slot, encoding).ok()) } pub fn get_confirmed_blocks( @@ -562,6 +567,7 @@ pub trait RpcSol { &self, meta: Self::Metadata, slot: Slot, + encoding: Option, ) -> Result>; #[rpc(meta, name = "getBlockTime")] @@ -1031,11 +1037,12 @@ impl RpcSol for RpcSolImpl { &self, meta: Self::Metadata, slot: Slot, + encoding: Option, ) -> Result> { meta.request_processor .read() .unwrap() - .get_confirmed_block(slot) + .get_confirmed_block(slot, encoding) } fn get_confirmed_blocks( @@ -1063,7 +1070,9 @@ pub mod tests { genesis_utils::{create_genesis_config, GenesisConfigInfo}, replay_stage::tests::create_test_transactions_and_populate_blocktree, }; + use bincode::deserialize; use jsonrpc_core::{MetaIoHandler, Output, Response, Value}; + use solana_client::rpc_request::RpcEncodedTransaction; use solana_ledger::{ blocktree::entries_to_test_shreds, blocktree_processor::fill_blocktree_slot_with_ticks, entry::next_entry_mut, get_tmp_ledger_path, @@ -2008,6 +2017,36 @@ pub mod tests { let req = format!(r#"{{"jsonrpc":"2.0","id":1,"method":"getConfirmedBlock","params":[0]}}"#); + let res = io.handle_request_sync(&req, meta.clone()); + let result: Value = serde_json::from_str(&res.expect("actual response")) + .expect("actual response deserialization"); + let confirmed_block: Option = + serde_json::from_value(result["result"].clone()).unwrap(); + let confirmed_block = confirmed_block.unwrap(); + assert_eq!(confirmed_block.transactions.len(), 3); + + for (transaction, result) in confirmed_block.transactions.into_iter() { + if let RpcEncodedTransaction::Json(transaction) = transaction { + if transaction.signatures[0] == confirmed_block_signatures[0].to_string() { + assert_eq!(transaction.message.recent_blockhash, blockhash.to_string()); + assert_eq!(result.unwrap().status, Ok(())); + } else if transaction.signatures[0] == confirmed_block_signatures[1].to_string() { + assert_eq!( + result.unwrap().status, + Err(TransactionError::InstructionError( + 0, + InstructionError::CustomError(1) + )) + ); + } else { + assert_eq!(result, None); + } + } + } + + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"getConfirmedBlock","params":[0, "binary"]}}"# + ); let res = io.handle_request_sync(&req, meta); let result: Value = serde_json::from_str(&res.expect("actual response")) .expect("actual response deserialization"); @@ -2017,19 +2056,23 @@ pub mod tests { assert_eq!(confirmed_block.transactions.len(), 3); for (transaction, result) in confirmed_block.transactions.into_iter() { - if transaction.signatures[0] == confirmed_block_signatures[0] { - assert_eq!(transaction.message.recent_blockhash, blockhash); - assert_eq!(result.unwrap().status, Ok(())); - } else if transaction.signatures[0] == confirmed_block_signatures[1] { - assert_eq!( - result.unwrap().status, - Err(TransactionError::InstructionError( - 0, - InstructionError::CustomError(1) - )) - ); - } else { - assert_eq!(result, None); + if let RpcEncodedTransaction::Binary(transaction) = transaction { + let decoded_transaction: Transaction = + deserialize(&bs58::decode(&transaction).into_vec().unwrap()).unwrap(); + if decoded_transaction.signatures[0] == confirmed_block_signatures[0] { + assert_eq!(decoded_transaction.message.recent_blockhash, blockhash); + assert_eq!(result.unwrap().status, Ok(())); + } else if decoded_transaction.signatures[0] == confirmed_block_signatures[1] { + assert_eq!( + result.unwrap().status, + Err(TransactionError::InstructionError( + 0, + InstructionError::CustomError(1) + )) + ); + } else { + assert_eq!(result, None); + } } } } diff --git a/ledger/src/blocktree.rs b/ledger/src/blocktree.rs index e4774d26ed..edd21bbdfa 100644 --- a/ledger/src/blocktree.rs +++ b/ledger/src/blocktree.rs @@ -21,7 +21,9 @@ use rayon::{ ThreadPool, }; use rocksdb::DBRawIterator; -use solana_client::rpc_request::{RpcConfirmedBlock, RpcTransactionStatus}; +use solana_client::rpc_request::{ + RpcConfirmedBlock, RpcEncodedTransaction, RpcTransactionEncoding, RpcTransactionStatus, +}; use solana_measure::measure::Measure; use solana_metrics::{datapoint_debug, datapoint_error}; use solana_rayon_threadlimit::get_thread_count; @@ -1324,7 +1326,12 @@ impl Blocktree { .collect() } - pub fn get_confirmed_block(&self, slot: Slot) -> Result { + pub fn get_confirmed_block( + &self, + slot: Slot, + encoding: Option, + ) -> Result { + let encoding = encoding.unwrap_or(RpcTransactionEncoding::Json); if self.is_root(slot) { let slot_meta_cf = self.db.column::(); let slot_meta = slot_meta_cf @@ -1344,13 +1351,18 @@ impl Blocktree { Hash::default() }; + let blockhash = get_last_hash(slot_entries.iter()) + .unwrap_or_else(|| panic!("Rooted slot {:?} must have blockhash", slot)); + let block = RpcConfirmedBlock { - previous_blockhash, - blockhash: get_last_hash(slot_entries.iter()) - .unwrap_or_else(|| panic!("Rooted slot {:?} must have blockhash", slot)), + previous_blockhash: previous_blockhash.to_string(), + blockhash: blockhash.to_string(), parent_slot: slot_meta.parent_slot, - transactions: self - .map_transactions_to_statuses(slot, slot_transaction_iterator), + transactions: self.map_transactions_to_statuses( + slot, + encoding, + slot_transaction_iterator, + ), }; return Ok(block); } @@ -1361,13 +1373,16 @@ impl Blocktree { fn map_transactions_to_statuses<'a>( &self, slot: Slot, + encoding: RpcTransactionEncoding, iterator: impl Iterator + 'a, - ) -> Vec<(Transaction, Option)> { + ) -> Vec<(RpcEncodedTransaction, Option)> { iterator .map(|transaction| { let signature = transaction.signatures[0]; + let encoded_transaction = + RpcEncodedTransaction::encode(transaction, encoding.clone()); ( - transaction, + encoded_transaction, self.transaction_status_cf .get((slot, signature)) .expect("Expect database get to succeed"), @@ -4634,31 +4649,52 @@ pub mod tests { .collect(); // Even if marked as root, a slot that is empty of entries should return an error - let confirmed_block_err = ledger.get_confirmed_block(slot - 1).unwrap_err(); + let confirmed_block_err = ledger.get_confirmed_block(slot - 1, None).unwrap_err(); assert_matches!(confirmed_block_err, BlocktreeError::SlotNotRooted); - let confirmed_block = ledger.get_confirmed_block(slot).unwrap(); + let confirmed_block = ledger.get_confirmed_block(slot, None).unwrap(); assert_eq!(confirmed_block.transactions.len(), 100); - let mut expected_block = RpcConfirmedBlock::default(); - expected_block.transactions = expected_transactions.clone(); - expected_block.parent_slot = slot - 1; - expected_block.blockhash = blockhash; + let expected_block = RpcConfirmedBlock { + transactions: expected_transactions + .iter() + .cloned() + .map(|(tx, status)| { + ( + RpcEncodedTransaction::encode(tx, RpcTransactionEncoding::Json), + status, + ) + }) + .collect(), + parent_slot: slot - 1, + blockhash: blockhash.to_string(), + previous_blockhash: Hash::default().to_string(), + }; // The previous_blockhash of `expected_block` is default because its parent slot is a // root, but empty of entries. This is special handling for snapshot root slots. assert_eq!(confirmed_block, expected_block); - let confirmed_block = ledger.get_confirmed_block(slot + 1).unwrap(); + let confirmed_block = ledger.get_confirmed_block(slot + 1, None).unwrap(); assert_eq!(confirmed_block.transactions.len(), 100); - let mut expected_block = RpcConfirmedBlock::default(); - expected_block.transactions = expected_transactions; - expected_block.parent_slot = slot; - expected_block.previous_blockhash = blockhash; - expected_block.blockhash = blockhash; + let expected_block = RpcConfirmedBlock { + transactions: expected_transactions + .iter() + .cloned() + .map(|(tx, status)| { + ( + RpcEncodedTransaction::encode(tx, RpcTransactionEncoding::Json), + status, + ) + }) + .collect(), + parent_slot: slot, + blockhash: blockhash.to_string(), + previous_blockhash: blockhash.to_string(), + }; assert_eq!(confirmed_block, expected_block); - let not_root = ledger.get_confirmed_block(slot + 2).unwrap_err(); + let not_root = ledger.get_confirmed_block(slot + 2, None).unwrap_err(); assert_matches!(not_root, BlocktreeError::SlotNotRooted); drop(ledger); @@ -4875,7 +4911,11 @@ pub mod tests { vec![CompiledInstruction::new(1, &(), vec![0])], )); - let map = blocktree.map_transactions_to_statuses(slot, transactions.into_iter()); + let map = blocktree.map_transactions_to_statuses( + slot, + RpcTransactionEncoding::Json, + transactions.into_iter(), + ); assert_eq!(map.len(), 5); for x in 0..4 { assert_eq!(map[x].1.as_ref().unwrap().fee, x as u64);