Add RPC support for versioned transactions (#22530)

* Add RPC support for versioned transactions

* fix doc tests

* Add rpc test for versioned txs

* Switch to preflight bank
This commit is contained in:
Justin Starry
2022-03-08 15:20:34 +08:00
committed by GitHub
parent e790d0fc53
commit 3114c199bd
29 changed files with 864 additions and 310 deletions

View File

@@ -18,7 +18,7 @@ pub mod token_balances;
pub use {crate::extract_memos::extract_and_fmt_memos, solana_runtime::bank::RewardType};
use {
crate::{
parse_accounts::{parse_accounts, ParsedAccount},
parse_accounts::{parse_accounts, parse_static_accounts, ParsedAccount},
parse_instruction::{parse, ParsedInstruction},
},
solana_account_decoder::parse_token::UiTokenAmount,
@@ -26,18 +26,48 @@ use {
clock::{Slot, UnixTimestamp},
commitment_config::CommitmentConfig,
instruction::CompiledInstruction,
message::{v0::LoadedAddresses, AccountKeys, Message, MessageHeader},
message::{
v0::{self, LoadedAddresses, LoadedMessage, MessageAddressTableLookup},
AccountKeys, Message, MessageHeader, VersionedMessage,
},
pubkey::Pubkey,
sanitize::Sanitize,
signature::Signature,
transaction::{Result, Transaction, TransactionError, VersionedTransaction},
transaction::{
Result as TransactionResult, Transaction, TransactionError, TransactionVersion,
VersionedTransaction,
},
},
std::fmt,
thiserror::Error,
};
pub struct BlockEncodingOptions {
pub transaction_details: TransactionDetails,
pub show_rewards: bool,
pub max_supported_transaction_version: Option<u8>,
}
#[derive(Error, Debug, PartialEq, Eq, Clone)]
pub enum EncodeError {
#[error("Encoding does not support transaction version {0}")]
UnsupportedTransactionVersion(u8),
}
/// Represents types that can be encoded into one of several encoding formats
pub trait Encodable {
type Encoded;
fn encode(self, encoding: UiTransactionEncoding) -> Self::Encoded;
fn encode(&self, encoding: UiTransactionEncoding) -> Self::Encoded;
}
/// Represents types that can be encoded into one of several encoding formats
pub trait EncodableWithMeta {
type Encoded;
fn encode_with_meta(
&self,
encoding: UiTransactionEncoding,
meta: &TransactionStatusMeta,
) -> Self::Encoded;
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -160,14 +190,13 @@ pub struct UiInnerInstructions {
}
impl UiInnerInstructions {
fn parse(inner_instructions: InnerInstructions, message: &Message) -> Self {
let account_keys = AccountKeys::new(&message.account_keys, None);
fn parse(inner_instructions: InnerInstructions, account_keys: &AccountKeys) -> Self {
Self {
index: inner_instructions.index,
instructions: inner_instructions
.instructions
.iter()
.map(|ix| UiInstruction::parse(ix, &account_keys))
.map(|ix| UiInstruction::parse(ix, account_keys))
.collect(),
}
}
@@ -221,7 +250,7 @@ impl From<TransactionTokenBalance> for UiTransactionTokenBalance {
#[derive(Clone, Debug, PartialEq)]
pub struct TransactionStatusMeta {
pub status: Result<()>,
pub status: TransactionResult<()>,
pub fee: u64,
pub pre_balances: Vec<u64>,
pub post_balances: Vec<u64>,
@@ -255,7 +284,7 @@ impl Default for TransactionStatusMeta {
#[serde(rename_all = "camelCase")]
pub struct UiTransactionStatusMeta {
pub err: Option<TransactionError>,
pub status: Result<()>, // This field is deprecated. See https://github.com/solana-labs/solana/issues/9302
pub status: TransactionResult<()>, // This field is deprecated. See https://github.com/solana-labs/solana/issues/9302
pub fee: u64,
pub pre_balances: Vec<u64>,
pub post_balances: Vec<u64>,
@@ -264,10 +293,38 @@ pub struct UiTransactionStatusMeta {
pub pre_token_balances: Option<Vec<UiTransactionTokenBalance>>,
pub post_token_balances: Option<Vec<UiTransactionTokenBalance>>,
pub rewards: Option<Rewards>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub loaded_addresses: Option<UiLoadedAddresses>,
}
/// A duplicate representation of LoadedAddresses
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UiLoadedAddresses {
pub writable: Vec<String>,
pub readonly: Vec<String>,
}
impl From<&LoadedAddresses> for UiLoadedAddresses {
fn from(loaded_addresses: &LoadedAddresses) -> Self {
Self {
writable: loaded_addresses
.writable
.iter()
.map(ToString::to_string)
.collect(),
readonly: loaded_addresses
.readonly
.iter()
.map(ToString::to_string)
.collect(),
}
}
}
impl UiTransactionStatusMeta {
fn parse(meta: TransactionStatusMeta, message: &Message) -> Self {
fn parse(meta: TransactionStatusMeta, static_keys: &[Pubkey]) -> Self {
let account_keys = AccountKeys::new(static_keys, Some(&meta.loaded_addresses));
Self {
err: meta.status.clone().err(),
status: meta.status,
@@ -276,7 +333,7 @@ impl UiTransactionStatusMeta {
post_balances: meta.post_balances,
inner_instructions: meta.inner_instructions.map(|ixs| {
ixs.into_iter()
.map(|ix| UiInnerInstructions::parse(ix, message))
.map(|ix| UiInnerInstructions::parse(ix, &account_keys))
.collect()
}),
log_messages: meta.log_messages,
@@ -287,6 +344,7 @@ impl UiTransactionStatusMeta {
.post_token_balances
.map(|balance| balance.into_iter().map(Into::into).collect()),
rewards: meta.rewards,
loaded_addresses: Some(UiLoadedAddresses::from(&meta.loaded_addresses)),
}
}
}
@@ -310,6 +368,7 @@ impl From<TransactionStatusMeta> for UiTransactionStatusMeta {
.post_token_balances
.map(|balance| balance.into_iter().map(Into::into).collect()),
rewards: meta.rewards,
loaded_addresses: Some(UiLoadedAddresses::from(&meta.loaded_addresses)),
}
}
}
@@ -326,8 +385,8 @@ pub enum TransactionConfirmationStatus {
#[serde(rename_all = "camelCase")]
pub struct TransactionStatus {
pub slot: Slot,
pub confirmations: Option<usize>, // None = rooted
pub status: Result<()>, // legacy field
pub confirmations: Option<usize>, // None = rooted
pub status: TransactionResult<()>, // legacy field
pub err: Option<TransactionError>,
pub confirmation_status: Option<TransactionConfirmationStatus>,
}
@@ -462,39 +521,21 @@ impl From<VersionedConfirmedBlock> for ConfirmedBlock {
}
}
impl Encodable for LegacyConfirmedBlock {
type Encoded = EncodedConfirmedBlock;
fn encode(self, encoding: UiTransactionEncoding) -> Self::Encoded {
Self::Encoded {
previous_blockhash: self.previous_blockhash,
blockhash: self.blockhash,
parent_slot: self.parent_slot,
transactions: self
.transactions
.into_iter()
.map(|tx| tx.encode(encoding))
.collect(),
rewards: self.rewards,
block_time: self.block_time,
block_height: self.block_height,
}
}
}
impl LegacyConfirmedBlock {
pub fn configure(
impl ConfirmedBlock {
pub fn encode_with_options(
self,
encoding: UiTransactionEncoding,
transaction_details: TransactionDetails,
show_rewards: bool,
) -> UiConfirmedBlock {
let (transactions, signatures) = match transaction_details {
options: BlockEncodingOptions,
) -> Result<UiConfirmedBlock, EncodeError> {
let (transactions, signatures) = match options.transaction_details {
TransactionDetails::Full => (
Some(
self.transactions
.into_iter()
.map(|tx_with_meta| tx_with_meta.encode(encoding))
.collect(),
.map(|tx_with_meta| {
tx_with_meta.encode(encoding, options.max_supported_transaction_version)
})
.collect::<Result<Vec<_>, _>>()?,
),
None,
),
@@ -503,26 +544,26 @@ impl LegacyConfirmedBlock {
Some(
self.transactions
.into_iter()
.map(|tx| tx.transaction.signatures[0].to_string())
.map(|tx_with_meta| tx_with_meta.transaction_signature().to_string())
.collect(),
),
),
TransactionDetails::None => (None, None),
};
UiConfirmedBlock {
Ok(UiConfirmedBlock {
previous_blockhash: self.previous_blockhash,
blockhash: self.blockhash,
parent_slot: self.parent_slot,
transactions,
signatures,
rewards: if show_rewards {
rewards: if options.show_rewards {
Some(self.rewards)
} else {
None
},
block_time: self.block_time,
block_height: self.block_height,
}
})
}
}
@@ -568,21 +609,6 @@ pub struct UiConfirmedBlock {
pub block_height: Option<u64>,
}
impl From<EncodedConfirmedBlock> for UiConfirmedBlock {
fn from(block: EncodedConfirmedBlock) -> Self {
Self {
previous_blockhash: block.previous_blockhash,
blockhash: block.blockhash,
parent_slot: block.parent_slot,
transactions: Some(block.transactions),
signatures: None,
rewards: Some(block.rewards),
block_time: block.block_time,
block_height: block.block_height,
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[allow(clippy::large_enum_variant)]
pub enum TransactionWithStatusMeta {
@@ -613,6 +639,23 @@ impl TransactionWithStatusMeta {
}
}
pub fn encode(
self,
encoding: UiTransactionEncoding,
max_supported_transaction_version: Option<u8>,
) -> Result<EncodedTransactionWithStatusMeta, EncodeError> {
match self {
Self::MissingMetadata(ref transaction) => Ok(EncodedTransactionWithStatusMeta {
version: None,
transaction: transaction.encode(encoding),
meta: None,
}),
Self::Complete(tx_with_meta) => {
tx_with_meta.encode(encoding, max_supported_transaction_version)
}
}
}
pub fn into_legacy_transaction_with_meta(self) -> Option<LegacyTransactionWithStatusMeta> {
match self {
TransactionWithStatusMeta::MissingMetadata(transaction) => {
@@ -626,9 +669,53 @@ impl TransactionWithStatusMeta {
}
}
}
pub fn account_keys(&self) -> AccountKeys {
match self {
Self::MissingMetadata(tx) => AccountKeys::new(&tx.message.account_keys, None),
Self::Complete(tx_with_meta) => tx_with_meta.account_keys(),
}
}
}
impl VersionedTransactionWithStatusMeta {
pub fn encode(
self,
encoding: UiTransactionEncoding,
max_supported_transaction_version: Option<u8>,
) -> Result<EncodedTransactionWithStatusMeta, EncodeError> {
let version = match (
max_supported_transaction_version,
self.transaction.version(),
) {
// Set to none because old clients can't handle this field
(None, TransactionVersion::LEGACY) => Ok(None),
(None, TransactionVersion::Number(version)) => {
Err(EncodeError::UnsupportedTransactionVersion(version))
}
(Some(_), TransactionVersion::LEGACY) => Ok(Some(TransactionVersion::LEGACY)),
(Some(max_version), TransactionVersion::Number(version)) => {
if version <= max_version {
Ok(Some(TransactionVersion::Number(version)))
} else {
Err(EncodeError::UnsupportedTransactionVersion(version))
}
}
}?;
Ok(EncodedTransactionWithStatusMeta {
transaction: self.transaction.encode_with_meta(encoding, &self.meta),
meta: Some(match encoding {
UiTransactionEncoding::JsonParsed => UiTransactionStatusMeta::parse(
self.meta,
self.transaction.message.static_account_keys(),
),
_ => UiTransactionStatusMeta::from(self.meta),
}),
version,
})
}
pub fn account_keys(&self) -> AccountKeys {
AccountKeys::new(
self.transaction.message.static_account_keys(),
@@ -636,7 +723,7 @@ impl VersionedTransactionWithStatusMeta {
)
}
pub fn into_legacy_transaction_with_meta(self) -> Option<LegacyTransactionWithStatusMeta> {
fn into_legacy_transaction_with_meta(self) -> Option<LegacyTransactionWithStatusMeta> {
Some(LegacyTransactionWithStatusMeta {
transaction: self.transaction.into_legacy_transaction()?,
meta: Some(self.meta),
@@ -644,26 +731,13 @@ impl VersionedTransactionWithStatusMeta {
}
}
impl Encodable for LegacyTransactionWithStatusMeta {
type Encoded = EncodedTransactionWithStatusMeta;
fn encode(self, encoding: UiTransactionEncoding) -> Self::Encoded {
Self::Encoded {
transaction: self.transaction.encode(encoding),
meta: self.meta.map(|meta| match encoding {
UiTransactionEncoding::JsonParsed => {
UiTransactionStatusMeta::parse(meta, &self.transaction.message)
}
_ => UiTransactionStatusMeta::from(meta),
}),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EncodedTransactionWithStatusMeta {
pub transaction: EncodedTransaction,
pub meta: Option<UiTransactionStatusMeta>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<TransactionVersion>,
}
#[derive(Debug, Clone, PartialEq)]
@@ -686,17 +760,6 @@ pub struct LegacyConfirmedTransactionWithStatusMeta {
pub block_time: Option<UnixTimestamp>,
}
impl Encodable for LegacyConfirmedTransactionWithStatusMeta {
type Encoded = EncodedConfirmedTransactionWithStatusMeta;
fn encode(self, encoding: UiTransactionEncoding) -> Self::Encoded {
Self::Encoded {
slot: self.slot,
transaction: self.tx_with_meta.encode(encoding),
block_time: self.block_time,
}
}
}
impl ConfirmedTransactionWithStatusMeta {
/// Downgrades a versioned confirmed transaction into a legacy
/// confirmed transaction if it contains a legacy transaction.
@@ -709,6 +772,20 @@ impl ConfirmedTransactionWithStatusMeta {
slot: self.slot,
})
}
pub fn encode(
self,
encoding: UiTransactionEncoding,
max_supported_transaction_version: Option<u8>,
) -> Result<EncodedConfirmedTransactionWithStatusMeta, EncodeError> {
Ok(EncodedConfirmedTransactionWithStatusMeta {
slot: self.slot,
transaction: self
.tx_with_meta
.encode(encoding, max_supported_transaction_version)?,
block_time: self.block_time,
})
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
@@ -728,9 +805,41 @@ pub enum EncodedTransaction {
Json(UiTransaction),
}
impl Encodable for &Transaction {
impl EncodableWithMeta for VersionedTransaction {
type Encoded = EncodedTransaction;
fn encode(self, encoding: UiTransactionEncoding) -> Self::Encoded {
fn encode_with_meta(
&self,
encoding: UiTransactionEncoding,
meta: &TransactionStatusMeta,
) -> Self::Encoded {
match encoding {
UiTransactionEncoding::Binary => EncodedTransaction::LegacyBinary(
bs58::encode(bincode::serialize(self).unwrap()).into_string(),
),
UiTransactionEncoding::Base58 => EncodedTransaction::Binary(
bs58::encode(bincode::serialize(self).unwrap()).into_string(),
encoding,
),
UiTransactionEncoding::Base64 => EncodedTransaction::Binary(
base64::encode(bincode::serialize(self).unwrap()),
encoding,
),
UiTransactionEncoding::Json | UiTransactionEncoding::JsonParsed => {
EncodedTransaction::Json(UiTransaction {
signatures: self.signatures.iter().map(ToString::to_string).collect(),
message: match &self.message {
VersionedMessage::Legacy(message) => message.encode(encoding),
VersionedMessage::V0(message) => message.encode_with_meta(encoding, meta),
},
})
}
}
}
}
impl Encodable for Transaction {
type Encoded = EncodedTransaction;
fn encode(&self, encoding: UiTransactionEncoding) -> Self::Encoded {
match encoding {
UiTransactionEncoding::Binary => EncodedTransaction::LegacyBinary(
bs58::encode(bincode::serialize(self).unwrap()).into_string(),
@@ -793,9 +902,9 @@ pub enum UiMessage {
Raw(UiRawMessage),
}
impl Encodable for &Message {
impl Encodable for Message {
type Encoded = UiMessage;
fn encode(self, encoding: UiTransactionEncoding) -> Self::Encoded {
fn encode(&self, encoding: UiTransactionEncoding) -> Self::Encoded {
if encoding == UiTransactionEncoding::JsonParsed {
let account_keys = AccountKeys::new(&self.account_keys, None);
UiMessage::Parsed(UiParsedMessage {
@@ -806,6 +915,7 @@ impl Encodable for &Message {
.iter()
.map(|instruction| UiInstruction::parse(instruction, &account_keys))
.collect(),
address_table_lookups: None,
})
} else {
UiMessage::Raw(UiRawMessage {
@@ -813,6 +923,43 @@ impl Encodable for &Message {
account_keys: self.account_keys.iter().map(ToString::to_string).collect(),
recent_blockhash: self.recent_blockhash.to_string(),
instructions: self.instructions.iter().map(Into::into).collect(),
address_table_lookups: None,
})
}
}
}
impl EncodableWithMeta for v0::Message {
type Encoded = UiMessage;
fn encode_with_meta(
&self,
encoding: UiTransactionEncoding,
meta: &TransactionStatusMeta,
) -> Self::Encoded {
if encoding == UiTransactionEncoding::JsonParsed {
let account_keys = AccountKeys::new(&self.account_keys, Some(&meta.loaded_addresses));
let loaded_message = LoadedMessage::new_borrowed(self, &meta.loaded_addresses);
UiMessage::Parsed(UiParsedMessage {
account_keys: parse_static_accounts(&loaded_message),
recent_blockhash: self.recent_blockhash.to_string(),
instructions: self
.instructions
.iter()
.map(|instruction| UiInstruction::parse(instruction, &account_keys))
.collect(),
address_table_lookups: Some(
self.address_table_lookups.iter().map(Into::into).collect(),
),
})
} else {
UiMessage::Raw(UiRawMessage {
header: self.header,
account_keys: self.account_keys.iter().map(ToString::to_string).collect(),
recent_blockhash: self.recent_blockhash.to_string(),
instructions: self.instructions.iter().map(Into::into).collect(),
address_table_lookups: Some(
self.address_table_lookups.iter().map(Into::into).collect(),
),
})
}
}
@@ -826,6 +973,27 @@ pub struct UiRawMessage {
pub account_keys: Vec<String>,
pub recent_blockhash: String,
pub instructions: Vec<UiCompiledInstruction>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub address_table_lookups: Option<Vec<UiAddressTableLookup>>,
}
/// A duplicate representation of a MessageAddressTableLookup, in raw format, for pretty JSON serialization
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UiAddressTableLookup {
pub account_key: String,
pub writable_indexes: Vec<u8>,
pub readonly_indexes: Vec<u8>,
}
impl From<&MessageAddressTableLookup> for UiAddressTableLookup {
fn from(lookup: &MessageAddressTableLookup) -> Self {
Self {
account_key: lookup.account_key.to_string(),
writable_indexes: lookup.writable_indexes.clone(),
readonly_indexes: lookup.readonly_indexes.clone(),
}
}
}
/// A duplicate representation of a Message, in parsed format, for pretty JSON serialization
@@ -835,6 +1003,7 @@ pub struct UiParsedMessage {
pub account_keys: Vec<ParsedAccount>,
pub recent_blockhash: String,
pub instructions: Vec<UiInstruction>,
pub address_table_lookups: Option<Vec<UiAddressTableLookup>>,
}
// A serialized `Vec<TransactionByAddrInfo>` is stored in the `tx-by-addr` table. The row keys are

View File

@@ -1,4 +1,4 @@
use solana_sdk::message::Message;
use solana_sdk::message::{v0::LoadedMessage, Message};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
@@ -20,16 +20,34 @@ pub fn parse_accounts(message: &Message) -> Vec<ParsedAccount> {
accounts
}
pub fn parse_static_accounts(message: &LoadedMessage) -> Vec<ParsedAccount> {
let mut accounts: Vec<ParsedAccount> = vec![];
for (i, account_key) in message.static_account_keys().iter().enumerate() {
accounts.push(ParsedAccount {
pubkey: account_key.to_string(),
writable: message.is_writable(i),
signer: message.is_signer(i),
});
}
accounts
}
#[cfg(test)]
mod test {
use {super::*, solana_sdk::message::MessageHeader};
use {
super::*,
solana_sdk::{
message::{v0, v0::LoadedAddresses, MessageHeader},
pubkey::Pubkey,
},
};
#[test]
fn test_parse_accounts() {
let pubkey0 = solana_sdk::pubkey::new_rand();
let pubkey1 = solana_sdk::pubkey::new_rand();
let pubkey2 = solana_sdk::pubkey::new_rand();
let pubkey3 = solana_sdk::pubkey::new_rand();
let pubkey0 = Pubkey::new_unique();
let pubkey1 = Pubkey::new_unique();
let pubkey2 = Pubkey::new_unique();
let pubkey3 = Pubkey::new_unique();
let message = Message {
header: MessageHeader {
num_required_signatures: 2,
@@ -66,4 +84,53 @@ mod test {
]
);
}
#[test]
fn test_parse_static_accounts() {
let pubkey0 = Pubkey::new_unique();
let pubkey1 = Pubkey::new_unique();
let pubkey2 = Pubkey::new_unique();
let pubkey3 = Pubkey::new_unique();
let message = LoadedMessage::new(
v0::Message {
header: MessageHeader {
num_required_signatures: 2,
num_readonly_signed_accounts: 1,
num_readonly_unsigned_accounts: 1,
},
account_keys: vec![pubkey0, pubkey1, pubkey2, pubkey3],
..v0::Message::default()
},
LoadedAddresses {
writable: vec![Pubkey::new_unique()],
readonly: vec![Pubkey::new_unique()],
},
);
assert_eq!(
parse_static_accounts(&message),
vec![
ParsedAccount {
pubkey: pubkey0.to_string(),
writable: true,
signer: true,
},
ParsedAccount {
pubkey: pubkey1.to_string(),
writable: false,
signer: true,
},
ParsedAccount {
pubkey: pubkey2.to_string(),
writable: true,
signer: false,
},
ParsedAccount {
pubkey: pubkey3.to_string(),
writable: false,
signer: false,
},
]
);
}
}