RPC: Support versioned txs in getFeeForMessage API (#28217)

* RPC: Support versioned txs in getFeeForMessage API

* Update sdk/program/src/message/sanitized.rs

Co-authored-by: Tyera Eulberg <teulberg@gmail.com>

Co-authored-by: Tyera Eulberg <teulberg@gmail.com>
This commit is contained in:
Justin Starry 2022-10-05 00:00:34 +08:00 committed by GitHub
parent 39b37e2c6f
commit ddf95c181c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 235 additions and 96 deletions

View File

@ -112,7 +112,7 @@ pub fn get_fee_for_messages(
) -> Result<u64, CliError> {
Ok(messages
.iter()
.map(|message| rpc_client.get_fee_for_message(message))
.map(|message| rpc_client.get_fee_for_message(*message))
.collect::<Result<Vec<_>, _>>()?
.iter()
.sum())

View File

@ -1,5 +1,5 @@
#[cfg(not(target_os = "solana"))]
use solana_sdk::transaction::TransactionError;
use solana_program::message::AddressLoaderError;
use thiserror::Error;
#[derive(Debug, Error, PartialEq, Eq, Clone)]
@ -22,13 +22,13 @@ pub enum AddressLookupError {
}
#[cfg(not(target_os = "solana"))]
impl From<AddressLookupError> for TransactionError {
impl From<AddressLookupError> for AddressLoaderError {
fn from(err: AddressLookupError) -> Self {
match err {
AddressLookupError::LookupTableAccountNotFound => Self::AddressLookupTableNotFound,
AddressLookupError::InvalidAccountOwner => Self::InvalidAddressLookupTableOwner,
AddressLookupError::InvalidAccountData => Self::InvalidAddressLookupTableData,
AddressLookupError::InvalidLookupIndex => Self::InvalidAddressLookupTableIndex,
AddressLookupError::LookupTableAccountNotFound => Self::LookupTableAccountNotFound,
AddressLookupError::InvalidAccountOwner => Self::InvalidAccountOwner,
AddressLookupError::InvalidAccountData => Self::InvalidAccountData,
AddressLookupError::InvalidLookupIndex => Self::InvalidLookupIndex,
}
}
}

View File

@ -19,7 +19,8 @@ use {
http_sender::HttpSender,
mock_sender::MockSender,
rpc_client::{
GetConfirmedSignaturesForAddress2Config, RpcClientConfig, SerializableTransaction,
GetConfirmedSignaturesForAddress2Config, RpcClientConfig, SerializableMessage,
SerializableTransaction,
},
rpc_sender::*,
},
@ -47,10 +48,9 @@ use {
epoch_schedule::EpochSchedule,
fee_calculator::{FeeCalculator, FeeRateGovernor},
hash::Hash,
message::Message,
pubkey::Pubkey,
signature::Signature,
transaction::{self},
transaction,
},
solana_transaction_status::{
EncodedConfirmedBlock, EncodedConfirmedTransactionWithStatusMeta, TransactionStatus,
@ -5276,28 +5276,20 @@ impl RpcClient {
}
#[allow(deprecated)]
pub async fn get_fee_for_message(&self, message: &Message) -> ClientResult<u64> {
if self.get_node_version().await? < semver::Version::new(1, 9, 0) {
let fee_calculator = self
.get_fee_calculator_for_blockhash(&message.recent_blockhash)
.await?
.ok_or_else(|| ClientErrorKind::Custom("Invalid blockhash".to_string()))?;
Ok(fee_calculator
.lamports_per_signature
.saturating_mul(message.header.num_required_signatures as u64))
} else {
let serialized_encoded =
serialize_and_encode::<Message>(message, UiTransactionEncoding::Base64)?;
let result = self
.send::<Response<Option<u64>>>(
RpcRequest::GetFeeForMessage,
json!([serialized_encoded, self.commitment()]),
)
.await?;
result
.value
.ok_or_else(|| ClientErrorKind::Custom("Invalid blockhash".to_string()).into())
}
pub async fn get_fee_for_message(
&self,
message: &impl SerializableMessage,
) -> ClientResult<u64> {
let serialized_encoded = serialize_and_encode(message, UiTransactionEncoding::Base64)?;
let result = self
.send::<Response<Option<u64>>>(
RpcRequest::GetFeeForMessage,
json!([serialized_encoded, self.commitment()]),
)
.await?;
result
.value
.ok_or_else(|| ClientErrorKind::Custom("Invalid blockhash".to_string()).into())
}
pub async fn get_new_latest_blockhash(&self, blockhash: &Hash) -> ClientResult<Hash> {

View File

@ -41,7 +41,7 @@ use {
epoch_schedule::EpochSchedule,
fee_calculator::{FeeCalculator, FeeRateGovernor},
hash::Hash,
message::Message,
message::{v0, Message as LegacyMessage},
pubkey::Pubkey,
signature::Signature,
transaction::{self, uses_durable_nonce, Transaction, VersionedTransaction},
@ -68,6 +68,12 @@ impl RpcClientConfig {
}
}
/// Trait used to add support for versioned messages to RPC APIs while
/// retaining backwards compatibility
pub trait SerializableMessage: Serialize {}
impl SerializableMessage for LegacyMessage {}
impl SerializableMessage for v0::Message {}
/// Trait used to add support for versioned transactions to RPC APIs while
/// retaining backwards compatibility
pub trait SerializableTransaction: Serialize {
@ -3975,7 +3981,7 @@ impl RpcClient {
}
#[allow(deprecated)]
pub fn get_fee_for_message(&self, message: &Message) -> ClientResult<u64> {
pub fn get_fee_for_message(&self, message: &impl SerializableMessage) -> ClientResult<u64> {
self.invoke((self.rpc_client.as_ref()).get_fee_for_message(message))
}

View File

@ -63,7 +63,7 @@ use {
feature_set,
fee_calculator::FeeCalculator,
hash::Hash,
message::{Message, SanitizedMessage},
message::SanitizedMessage,
pubkey::{Pubkey, PUBKEY_BYTES},
signature::{Keypair, Signature, Signer},
stake::state::{StakeActivationStatus, StakeState},
@ -2164,16 +2164,6 @@ impl JsonRpcRequestProcessor {
Ok(new_response(&bank, is_valid))
}
fn get_fee_for_message(
&self,
message: &SanitizedMessage,
config: RpcContextConfig,
) -> Result<RpcResponse<Option<u64>>> {
let bank = self.get_bank_with_config(config)?;
let fee = bank.get_fee_for_message(message);
Ok(new_response(&bank, fee))
}
fn get_stake_minimum_delegation(&self, config: RpcContextConfig) -> Result<RpcResponse<u64>> {
let bank = self.get_bank_with_config(config)?;
let stake_minimum_delegation =
@ -3288,7 +3278,10 @@ pub mod rpc_accounts {
// Full RPC interface that an API node is expected to provide
// (rpc_minimal should also be provided by an API node)
pub mod rpc_full {
use super::*;
use {
super::*,
solana_sdk::message::{SanitizedVersionedMessage, VersionedMessage},
};
#[rpc]
pub trait Full {
type Metadata;
@ -4001,12 +3994,21 @@ pub mod rpc_full {
config: Option<RpcContextConfig>,
) -> Result<RpcResponse<Option<u64>>> {
debug!("get_fee_for_message rpc request received");
let (_, message) =
decode_and_deserialize::<Message>(data, TransactionBinaryEncoding::Base64)?;
let sanitized_message = SanitizedMessage::try_from(message).map_err(|err| {
Error::invalid_params(format!("invalid transaction message: {}", err))
})?;
meta.get_fee_for_message(&sanitized_message, config.unwrap_or_default())
let (_, message) = decode_and_deserialize::<VersionedMessage>(
data,
TransactionBinaryEncoding::Base64,
)?;
let bank = &*meta.get_bank_with_config(config.unwrap_or_default())?;
let sanitized_versioned_message = SanitizedVersionedMessage::try_from(message)
.map_err(|err| {
Error::invalid_params(format!("invalid transaction message: {}", err))
})?;
let sanitized_message = SanitizedMessage::try_new(sanitized_versioned_message, bank)
.map_err(|err| {
Error::invalid_params(format!("invalid transaction message: {}", err))
})?;
let fee = bank.get_fee_for_message(&sanitized_message);
Ok(new_response(bank, fee))
}
fn get_stake_minimum_delegation(
@ -4649,10 +4651,13 @@ pub mod tests {
account::{Account, WritableAccount},
clock::MAX_RECENT_BLOCKHASHES,
compute_budget::ComputeBudgetInstruction,
fee_calculator::DEFAULT_BURN_PERCENT,
fee_calculator::{FeeRateGovernor, DEFAULT_BURN_PERCENT},
hash::{hash, Hash},
instruction::InstructionError,
message::{v0, v0::MessageAddressTableLookup, MessageHeader, VersionedMessage},
message::{
v0::{self, MessageAddressTableLookup},
Message, MessageHeader, VersionedMessage,
},
nonce::{self, state::DurableNonce},
rpc_port,
signature::{Keypair, Signer},
@ -4683,7 +4688,8 @@ pub mod tests {
std::{borrow::Cow, collections::HashMap},
};
const TEST_MINT_LAMPORTS: u64 = 1_000_000;
const TEST_MINT_LAMPORTS: u64 = 1_000_000_000;
const TEST_SIGNATURE_FEE: u64 = 5_000;
const TEST_SLOTS_PER_EPOCH: u64 = DELINQUENT_VALIDATOR_SLOT_DISTANCE + 1;
fn create_test_request(method: &str, params: Option<serde_json::Value>) -> serde_json::Value {
@ -4829,8 +4835,12 @@ pub mod tests {
let keypair3 = Keypair::new();
let bank = self.working_bank();
let rent_exempt_amount = bank.get_minimum_balance_for_rent_exemption(0);
bank.transfer(rent_exempt_amount, mint_keypair, &keypair2.pubkey())
.unwrap();
bank.transfer(
rent_exempt_amount + TEST_SIGNATURE_FEE,
mint_keypair,
&keypair2.pubkey(),
)
.unwrap();
let (entries, signatures) = create_test_transaction_entries(
vec![&self.mint_keypair, &keypair1, &keypair2, &keypair3],
@ -5471,7 +5481,7 @@ pub mod tests {
"context": {"slot": 0, "apiVersion": RpcApiVersion::default()},
"value":{
"owner": "11111111111111111111111111111111",
"lamports": 1_000_000,
"lamports": TEST_MINT_LAMPORTS,
"data": "",
"executable": false,
"rentEpoch": 0
@ -5548,7 +5558,7 @@ pub mod tests {
let expected = json!([
{
"owner": "11111111111111111111111111111111",
"lamports": 1_000_000,
"lamports": TEST_MINT_LAMPORTS,
"data": ["", "base64"],
"executable": false,
"rentEpoch": 0
@ -5580,7 +5590,7 @@ pub mod tests {
let expected = json!([
{
"owner": "11111111111111111111111111111111",
"lamports": 1_000_000,
"lamports": TEST_MINT_LAMPORTS,
"data": ["", "base58"],
"executable": false,
"rentEpoch": 0
@ -6167,7 +6177,7 @@ pub mod tests {
"value":{
"blockhash": recent_blockhash.to_string(),
"feeCalculator": {
"lamportsPerSignature": 0,
"lamportsPerSignature": TEST_SIGNATURE_FEE,
}
},
},
@ -6196,7 +6206,7 @@ pub mod tests {
"value": {
"blockhash": recent_blockhash.to_string(),
"feeCalculator": {
"lamportsPerSignature": 0,
"lamportsPerSignature": TEST_SIGNATURE_FEE,
},
"lastValidSlot": MAX_RECENT_BLOCKHASHES,
"lastValidBlockHeight": MAX_RECENT_BLOCKHASHES,
@ -6276,9 +6286,9 @@ pub mod tests {
"value":{
"feeRateGovernor": {
"burnPercent": DEFAULT_BURN_PERCENT,
"maxLamportsPerSignature": 0,
"minLamportsPerSignature": 0,
"targetLamportsPerSignature": 0,
"maxLamportsPerSignature": TEST_SIGNATURE_FEE,
"minLamportsPerSignature": TEST_SIGNATURE_FEE,
"targetLamportsPerSignature": TEST_SIGNATURE_FEE,
"targetSignaturesPerSlot": 0
}
},
@ -6547,6 +6557,7 @@ pub mod tests {
genesis_config.rent.exemption_threshold = 2.0;
genesis_config.epoch_schedule =
EpochSchedule::custom(TEST_SLOTS_PER_EPOCH, TEST_SLOTS_PER_EPOCH, false);
genesis_config.fee_rate_governor = FeeRateGovernor::new(TEST_SIGNATURE_FEE, 0);
let bank = Bank::new_for_tests(&genesis_config);
(
@ -8457,7 +8468,7 @@ pub mod tests {
assert_eq!(
sanitize_transaction(versioned_tx, SimpleAddressLoader::Disabled).unwrap_err(),
Error::invalid_params(
"invalid transaction: Transaction loads an address table account that doesn't exist".to_string(),
"invalid transaction: Transaction version is unsupported".to_string(),
)
);
}
@ -8479,6 +8490,54 @@ pub mod tests {
);
}
#[test]
fn test_get_fee_for_message() {
let rpc = RpcHandler::start();
let bank = rpc.working_bank();
// Slot hashes is necessary for processing versioned txs.
bank.set_sysvar_for_tests(&SlotHashes::default());
// Correct blockhash is needed because fees are specific to blockhashes
let recent_blockhash = bank.last_blockhash();
{
let legacy_msg = VersionedMessage::Legacy(Message {
header: MessageHeader {
num_required_signatures: 1,
..MessageHeader::default()
},
recent_blockhash,
account_keys: vec![Pubkey::new_unique()],
..Message::default()
});
let request = create_test_request(
"getFeeForMessage",
Some(json!([base64::encode(&serialize(&legacy_msg).unwrap())])),
);
let response: RpcResponse<u64> = parse_success_result(rpc.handle_request_sync(request));
assert_eq!(response.value, TEST_SIGNATURE_FEE);
}
{
let v0_msg = VersionedMessage::V0(v0::Message {
header: MessageHeader {
num_required_signatures: 1,
..MessageHeader::default()
},
recent_blockhash,
account_keys: vec![Pubkey::new_unique()],
..v0::Message::default()
});
let request = create_test_request(
"getFeeForMessage",
Some(json!([base64::encode(&serialize(&v0_msg).unwrap())])),
);
let response: RpcResponse<u64> = parse_success_result(rpc.handle_request_sync(request));
assert_eq!(response.value, TEST_SIGNATURE_FEE);
}
}
#[test]
fn test_rpc_get_recent_prioritization_fees() {
fn wait_for_cache_blocks(cache: &PrioritizationFeeCache, num_blocks: usize) {

View File

@ -2,8 +2,11 @@ use {
super::Bank,
solana_address_lookup_table_program::error::AddressLookupError,
solana_sdk::{
message::v0::{LoadedAddresses, MessageAddressTableLookup},
transaction::{AddressLoader, Result as TransactionResult, TransactionError},
message::{
v0::{LoadedAddresses, MessageAddressTableLookup},
AddressLoaderError,
},
transaction::AddressLoader,
},
};
@ -11,9 +14,9 @@ impl AddressLoader for &Bank {
fn load_addresses(
self,
address_table_lookups: &[MessageAddressTableLookup],
) -> TransactionResult<LoadedAddresses> {
) -> Result<LoadedAddresses, AddressLoaderError> {
if !self.versioned_tx_message_enabled() {
return Err(TransactionError::UnsupportedVersion);
return Err(AddressLoaderError::Disabled);
}
let slot_hashes = self
@ -21,7 +24,7 @@ impl AddressLoader for &Bank {
.read()
.unwrap()
.get_slot_hashes()
.map_err(|_| TransactionError::AccountNotFound)?;
.map_err(|_| AddressLoaderError::SlotHashesSysvarNotFound)?;
Ok(address_table_lookups
.iter()

View File

@ -0,0 +1,56 @@
use {
super::v0::{LoadedAddresses, MessageAddressTableLookup},
thiserror::Error,
};
#[derive(Debug, Error, PartialEq, Eq, Clone)]
pub enum AddressLoaderError {
/// Address loading from lookup tables is disabled
#[error("Address loading from lookup tables is disabled")]
Disabled,
/// Failed to load slot hashes sysvar
#[error("Failed to load slot hashes sysvar")]
SlotHashesSysvarNotFound,
/// Attempted to lookup addresses from a table that does not exist
#[error("Attempted to lookup addresses from a table that does not exist")]
LookupTableAccountNotFound,
/// Attempted to lookup addresses from an account owned by the wrong program
#[error("Attempted to lookup addresses from an account owned by the wrong program")]
InvalidAccountOwner,
/// Attempted to lookup addresses from an invalid account
#[error("Attempted to lookup addresses from an invalid account")]
InvalidAccountData,
/// Address lookup contains an invalid index
#[error("Address lookup contains an invalid index")]
InvalidLookupIndex,
}
pub trait AddressLoader: Clone {
fn load_addresses(
self,
lookups: &[MessageAddressTableLookup],
) -> Result<LoadedAddresses, AddressLoaderError>;
}
#[derive(Clone)]
pub enum SimpleAddressLoader {
Disabled,
Enabled(LoadedAddresses),
}
impl AddressLoader for SimpleAddressLoader {
fn load_addresses(
self,
_lookups: &[MessageAddressTableLookup],
) -> Result<LoadedAddresses, AddressLoaderError> {
match self {
Self::Disabled => Err(AddressLoaderError::Disabled),
Self::Enabled(loaded_addresses) => Ok(loaded_addresses),
}
}
}

View File

@ -44,10 +44,11 @@ pub mod legacy;
#[path = ""]
mod non_bpf_modules {
mod account_keys;
mod address_loader;
mod sanitized;
mod versions;
pub use {account_keys::*, sanitized::*, versions::*};
pub use {account_keys::*, address_loader::*, sanitized::*, versions::*};
}
use compiled_keys::*;

View File

@ -5,7 +5,8 @@ use {
message::{
legacy,
v0::{self, LoadedAddresses},
AccountKeys, MessageHeader,
AccountKeys, AddressLoader, AddressLoaderError, MessageHeader,
SanitizedVersionedMessage, VersionedMessage,
},
nonce::NONCED_TX_MARKER_IX_INDEX,
program_utils::limited_deserialize,
@ -81,6 +82,8 @@ pub enum SanitizeMessageError {
ValueOutOfBounds,
#[error("invalid value")]
InvalidValue,
#[error("{0}")]
AddressLoaderError(#[from] AddressLoaderError),
}
impl From<SanitizeError> for SanitizeMessageError {
@ -102,6 +105,25 @@ impl TryFrom<legacy::Message> for SanitizedMessage {
}
impl SanitizedMessage {
/// Create a sanitized message from a sanitized versioned message.
/// If the input message uses address tables, attempt to look up the
/// address for each table index.
pub fn try_new(
sanitized_msg: SanitizedVersionedMessage,
address_loader: impl AddressLoader,
) -> Result<Self, SanitizeMessageError> {
Ok(match sanitized_msg.message {
VersionedMessage::Legacy(message) => {
SanitizedMessage::Legacy(LegacyMessage::new(message))
}
VersionedMessage::V0(message) => {
let loaded_addresses =
address_loader.load_addresses(&message.address_table_lookups)?;
SanitizedMessage::V0(v0::LoadedMessage::new(message, loaded_addresses))
}
})
}
/// Return true if this message contains duplicate account keys
pub fn has_duplicates(&self) -> bool {
match self {

View File

@ -1,6 +1,8 @@
use {
crate::{
instruction::InstructionError, message::SanitizeMessageError, sanitize::SanitizeError,
instruction::InstructionError,
message::{AddressLoaderError, SanitizeMessageError},
sanitize::SanitizeError,
},
serde::Serialize,
thiserror::Error,
@ -156,7 +158,23 @@ impl From<SanitizeError> for TransactionError {
}
impl From<SanitizeMessageError> for TransactionError {
fn from(_err: SanitizeMessageError) -> Self {
Self::SanitizeFailure
fn from(err: SanitizeMessageError) -> Self {
match err {
SanitizeMessageError::AddressLoaderError(err) => Self::from(err),
_ => Self::SanitizeFailure,
}
}
}
impl From<AddressLoaderError> for TransactionError {
fn from(err: AddressLoaderError) -> Self {
match err {
AddressLoaderError::Disabled => Self::UnsupportedVersion,
AddressLoaderError::SlotHashesSysvarNotFound => Self::AccountNotFound,
AddressLoaderError::LookupTableAccountNotFound => Self::AddressLookupTableNotFound,
AddressLoaderError::InvalidAccountOwner => Self::InvalidAddressLookupTableOwner,
AddressLoaderError::InvalidAccountData => Self::InvalidAddressLookupTableData,
AddressLoaderError::InvalidLookupIndex => Self::InvalidAddressLookupTableIndex,
}
}
}

View File

@ -1,12 +1,13 @@
#![cfg(feature = "full")]
pub use crate::message::{AddressLoader, SimpleAddressLoader};
use {
super::SanitizedVersionedTransaction,
crate::{
hash::Hash,
message::{
legacy,
v0::{self, LoadedAddresses, MessageAddressTableLookup},
v0::{self, LoadedAddresses},
LegacyMessage, SanitizedMessage, VersionedMessage,
},
precompiles::verify_if_precompile,
@ -43,25 +44,6 @@ pub struct TransactionAccountLocks<'a> {
pub writable: Vec<&'a Pubkey>,
}
pub trait AddressLoader: Clone {
fn load_addresses(self, lookups: &[MessageAddressTableLookup]) -> Result<LoadedAddresses>;
}
#[derive(Clone)]
pub enum SimpleAddressLoader {
Disabled,
Enabled(LoadedAddresses),
}
impl AddressLoader for SimpleAddressLoader {
fn load_addresses(self, _lookups: &[MessageAddressTableLookup]) -> Result<LoadedAddresses> {
match self {
Self::Disabled => Err(TransactionError::AddressLookupTableNotFound),
Self::Enabled(loaded_addresses) => Ok(loaded_addresses),
}
}
}
/// Type that represents whether the transaction message has been precomputed or
/// not.
pub enum MessageHash {