Store versioned transactions in the ledger, disabled by default (#19139)

* Add support for versioned transactions, but disable by default

* merge conflicts

* trent's feedback

* bump Cargo.lock

* Fix transaction error encoding

* Rename legacy_transaction method

* cargo clippy

* Clean up casts, int arithmetic, and unused methods

* Check for duplicates in sanitized message conversion

* fix clippy

* fix new test

* Fix bpf conditional compilation for message module
This commit is contained in:
Justin Starry
2021-08-17 15:17:56 -07:00
committed by GitHub
parent 098e2b2de3
commit c50b01cb60
47 changed files with 2373 additions and 1049 deletions

View File

@ -31,12 +31,11 @@ use solana_sdk::{
fee_calculator::FeeCalculator,
genesis_config::ClusterType,
hash::Hash,
message::Message,
message::SanitizedMessage,
native_loader, nonce,
nonce::NONCED_TX_MARKER_IX_INDEX,
pubkey::Pubkey,
transaction::Result,
transaction::{Transaction, TransactionError},
transaction::{Result, SanitizedTransaction, TransactionError},
};
use std::{
cmp::Reverse,
@ -198,7 +197,7 @@ impl Accounts {
}
}
fn construct_instructions_account(message: &Message) -> AccountSharedData {
fn construct_instructions_account(message: &SanitizedMessage) -> AccountSharedData {
let mut data = message.serialize_instructions();
// add room for current instruction index.
data.resize(data.len() + 2, 0);
@ -211,7 +210,7 @@ impl Accounts {
fn load_transaction(
&self,
ancestors: &Ancestors,
tx: &Transaction,
tx: &SanitizedTransaction,
fee: u64,
error_counters: &mut ErrorCounters,
rent_collector: &RentCollector,
@ -219,19 +218,20 @@ impl Accounts {
) -> Result<LoadedTransaction> {
// Copy all the accounts
let message = tx.message();
if tx.signatures.is_empty() && fee != 0 {
// NOTE: this check will never fail because `tx` is sanitized
if tx.signatures().is_empty() && fee != 0 {
Err(TransactionError::MissingSignatureForFee)
} else {
// There is no way to predict what program will execute without an error
// If a fee can pay for execution then the program will be scheduled
let mut payer_index = None;
let mut tx_rent: TransactionRent = 0;
let mut accounts = Vec::with_capacity(message.account_keys.len());
let mut account_deps = Vec::with_capacity(message.account_keys.len());
let mut accounts = Vec::with_capacity(message.account_keys_len());
let mut account_deps = Vec::with_capacity(message.account_keys_len());
let mut rent_debits = RentDebits::default();
let rent_for_sysvars = feature_set.is_active(&feature_set::rent_for_sysvars::id());
for (i, key) in message.account_keys.iter().enumerate() {
for (i, key) in message.account_keys_iter().enumerate() {
let account = if message.is_non_loader_key(i) {
if payer_index.is_none() {
payer_index = Some(i);
@ -296,7 +296,7 @@ impl Accounts {
};
accounts.push((*key, account));
}
debug_assert_eq!(accounts.len(), message.account_keys.len());
debug_assert_eq!(accounts.len(), message.account_keys_len());
// Appends the account_deps at the end of the accounts,
// this way they can be accessed in a uniform way.
// At places where only the accounts are needed,
@ -336,19 +336,9 @@ impl Accounts {
let message = tx.message();
let loaders = message
.instructions
.iter()
.map(|ix| {
if message.account_keys.len() <= ix.program_id_index as usize {
error_counters.account_not_found += 1;
return Err(TransactionError::AccountNotFound);
}
let program_id = message.account_keys[ix.program_id_index as usize];
self.load_executable_accounts(
ancestors,
&program_id,
error_counters,
)
.program_instructions_iter()
.map(|(program_id, _ix)| {
self.load_executable_accounts(ancestors, program_id, error_counters)
})
.collect::<Result<TransactionLoaders>>()?;
Ok(LoadedTransaction {
@ -434,17 +424,18 @@ impl Accounts {
Ok(accounts)
}
pub fn load_accounts<'a>(
pub fn load_accounts(
&self,
ancestors: &Ancestors,
txs: impl Iterator<Item = &'a Transaction>,
txs: &[SanitizedTransaction],
lock_results: Vec<TransactionCheckResult>,
hash_queue: &BlockhashQueue,
error_counters: &mut ErrorCounters,
rent_collector: &RentCollector,
feature_set: &FeatureSet,
) -> Vec<TransactionLoadResult> {
txs.zip(lock_results)
txs.iter()
.zip(lock_results)
.map(|etx| match etx {
(tx, (Ok(()), nonce_rollback)) => {
let fee_calculator = nonce_rollback
@ -453,12 +444,11 @@ impl Accounts {
.unwrap_or_else(|| {
#[allow(deprecated)]
hash_queue
.get_fee_calculator(&tx.message().recent_blockhash)
.get_fee_calculator(tx.message().recent_blockhash())
.cloned()
});
let fee = if let Some(fee_calculator) = fee_calculator {
#[allow(deprecated)]
fee_calculator.calculate_fee(tx.message())
tx.message().calculate_fee(&fee_calculator)
} else {
return (Err(TransactionError::BlockhashNotFound), None);
};
@ -879,15 +869,14 @@ impl Accounts {
/// same time
#[must_use]
#[allow(clippy::needless_collect)]
pub fn lock_accounts<'a>(&self, txs: impl Iterator<Item = &'a Transaction>) -> Vec<Result<()>> {
let keys: Vec<_> = txs
.map(|tx| tx.message().get_account_keys_by_lock_type())
.collect();
pub fn lock_accounts<'a>(
&self,
txs: impl Iterator<Item = &'a SanitizedTransaction>,
) -> Vec<Result<()>> {
let keys: Vec<_> = txs.map(|tx| tx.get_account_locks()).collect();
let mut account_locks = &mut self.account_locks.lock().unwrap();
keys.into_iter()
.map(|(writable_keys, readonly_keys)| {
self.lock_account(&mut account_locks, writable_keys, readonly_keys)
})
.map(|keys| self.lock_account(&mut account_locks, keys.writable, keys.readonly))
.collect()
}
@ -895,7 +884,7 @@ impl Accounts {
#[allow(clippy::needless_collect)]
pub fn unlock_accounts<'a>(
&self,
txs: impl Iterator<Item = &'a Transaction>,
txs: impl Iterator<Item = &'a SanitizedTransaction>,
results: &[Result<()>],
) {
let keys: Vec<_> = txs
@ -904,13 +893,13 @@ impl Accounts {
Err(TransactionError::AccountInUse) => None,
Err(TransactionError::SanitizeFailure) => None,
Err(TransactionError::AccountLoadedTwice) => None,
_ => Some(tx.message.get_account_keys_by_lock_type()),
_ => Some(tx.get_account_locks()),
})
.collect();
let mut account_locks = self.account_locks.lock().unwrap();
debug!("bank unlock accounts");
keys.into_iter().for_each(|(writable_keys, readonly_keys)| {
self.unlock_account(&mut account_locks, writable_keys, readonly_keys);
keys.into_iter().for_each(|keys| {
self.unlock_account(&mut account_locks, keys.writable, keys.readonly);
});
}
@ -920,7 +909,7 @@ impl Accounts {
pub fn store_cached<'a>(
&self,
slot: Slot,
txs: impl Iterator<Item = &'a Transaction>,
txs: &'a [SanitizedTransaction],
res: &'a [TransactionExecutionResult],
loaded: &'a mut [TransactionLoadResult],
rent_collector: &RentCollector,
@ -954,7 +943,7 @@ impl Accounts {
fn collect_accounts_to_store<'a>(
&self,
txs: impl Iterator<Item = &'a Transaction>,
txs: &'a [SanitizedTransaction],
res: &'a [TransactionExecutionResult],
loaded: &'a mut [TransactionLoadResult],
rent_collector: &RentCollector,
@ -991,10 +980,10 @@ impl Accounts {
(Err(_), _nonce_rollback) => continue,
};
let message = &tx.message();
let message = tx.message();
let loaded_transaction = raccs.as_mut().unwrap();
let mut fee_payer_index = None;
for (i, (key, account)) in (0..message.account_keys.len())
for (i, (key, account)) in (0..message.account_keys_len())
.zip(loaded_transaction.accounts.iter_mut())
.filter(|(i, _account)| message.is_non_loader_key(*i))
{
@ -1131,14 +1120,25 @@ mod tests {
message::Message,
nonce, nonce_account,
rent::Rent,
signature::{keypair_from_seed, Keypair, Signer},
signature::{keypair_from_seed, signers::Signers, Keypair, Signer},
system_instruction, system_program,
transaction::Transaction,
};
use std::{
convert::TryFrom,
sync::atomic::{AtomicBool, AtomicU64, Ordering},
{thread, time},
};
fn new_sanitized_tx<T: Signers>(
from_keypairs: &T,
message: Message,
recent_blockhash: Hash,
) -> SanitizedTransaction {
SanitizedTransaction::try_from(Transaction::new(from_keypairs, message, recent_blockhash))
.unwrap()
}
fn load_accounts_with_fee_and_rent(
tx: Transaction,
ka: &[(Pubkey, AccountSharedData)],
@ -1160,9 +1160,10 @@ mod tests {
}
let ancestors = vec![(0, 0)].into_iter().collect();
let sanitized_tx = SanitizedTransaction::try_from(tx).unwrap();
accounts.load_accounts(
&ancestors,
[tx].iter(),
&[sanitized_tx],
vec![(Ok(()), None)],
&hash_queue,
error_counters,
@ -1190,30 +1191,6 @@ mod tests {
load_accounts_with_fee(tx, ka, &fee_calculator, error_counters)
}
#[test]
fn test_load_accounts_no_key() {
let accounts: Vec<(Pubkey, AccountSharedData)> = Vec::new();
let mut error_counters = ErrorCounters::default();
let instructions = vec![CompiledInstruction::new(0, &(), vec![0])];
let tx = Transaction::new_with_compiled_instructions::<[&Keypair; 0]>(
&[],
&[],
Hash::default(),
vec![native_loader::id()],
instructions,
);
let loaded_accounts = load_accounts(tx, &accounts, &mut error_counters);
assert_eq!(error_counters.account_not_found, 1);
assert_eq!(loaded_accounts.len(), 1);
assert_eq!(
loaded_accounts[0],
(Err(TransactionError::AccountNotFound), None,)
);
}
#[test]
fn test_load_accounts_no_account_0_exists() {
let accounts: Vec<(Pubkey, AccountSharedData)> = Vec::new();
@ -1522,41 +1499,6 @@ mod tests {
);
}
#[test]
fn test_load_accounts_bad_program_id() {
let mut accounts: Vec<(Pubkey, AccountSharedData)> = Vec::new();
let mut error_counters = ErrorCounters::default();
let keypair = Keypair::new();
let key0 = keypair.pubkey();
let key1 = Pubkey::new(&[5u8; 32]);
let account = AccountSharedData::new(1, 0, &Pubkey::default());
accounts.push((key0, account));
let mut account = AccountSharedData::new(40, 1, &native_loader::id());
account.set_executable(true);
accounts.push((key1, account));
let instructions = vec![CompiledInstruction::new(0, &(), vec![0])];
let tx = Transaction::new_with_compiled_instructions(
&[&keypair],
&[],
Hash::default(),
vec![key1],
instructions,
);
let loaded_accounts = load_accounts(tx, &accounts, &mut error_counters);
assert_eq!(error_counters.invalid_program_for_execution, 1);
assert_eq!(loaded_accounts.len(), 1);
assert_eq!(
loaded_accounts[0],
(Err(TransactionError::InvalidProgramForExecution), None,)
);
}
#[test]
fn test_load_accounts_bad_owner() {
let mut accounts: Vec<(Pubkey, AccountSharedData)> = Vec::new();
@ -1784,7 +1726,7 @@ mod tests {
Hash::default(),
instructions,
);
let tx = Transaction::new(&[&keypair0], message, Hash::default());
let tx = new_sanitized_tx(&[&keypair0], message, Hash::default());
let results0 = accounts.lock_accounts([tx.clone()].iter());
assert!(results0[0].is_ok());
@ -1808,7 +1750,7 @@ mod tests {
Hash::default(),
instructions,
);
let tx0 = Transaction::new(&[&keypair2], message, Hash::default());
let tx0 = new_sanitized_tx(&[&keypair2], message, Hash::default());
let instructions = vec![CompiledInstruction::new(2, &(), vec![0, 1])];
let message = Message::new_with_compiled_instructions(
1,
@ -1818,7 +1760,7 @@ mod tests {
Hash::default(),
instructions,
);
let tx1 = Transaction::new(&[&keypair1], message, Hash::default());
let tx1 = new_sanitized_tx(&[&keypair1], message, Hash::default());
let txs = vec![tx0, tx1];
let results1 = accounts.lock_accounts(txs.iter());
@ -1846,7 +1788,7 @@ mod tests {
Hash::default(),
instructions,
);
let tx = Transaction::new(&[&keypair1], message, Hash::default());
let tx = new_sanitized_tx(&[&keypair1], message, Hash::default());
let results2 = accounts.lock_accounts([tx].iter());
assert!(results2[0].is_ok()); // Now keypair1 account can be locked as writable
@ -1895,7 +1837,7 @@ mod tests {
Hash::default(),
instructions,
);
let readonly_tx = Transaction::new(&[&keypair0], readonly_message, Hash::default());
let readonly_tx = new_sanitized_tx(&[&keypair0], readonly_message, Hash::default());
let instructions = vec![CompiledInstruction::new(2, &(), vec![0, 1])];
let writable_message = Message::new_with_compiled_instructions(
@ -1906,7 +1848,7 @@ mod tests {
Hash::default(),
instructions,
);
let writable_tx = Transaction::new(&[&keypair1], writable_message, Hash::default());
let writable_tx = new_sanitized_tx(&[&keypair1], writable_message, Hash::default());
let counter_clone = counter.clone();
let accounts_clone = accounts_arc.clone();
@ -1967,7 +1909,7 @@ mod tests {
(message.account_keys[0], account0),
(message.account_keys[1], account2.clone()),
];
let tx0 = Transaction::new(&[&keypair0], message, Hash::default());
let tx0 = new_sanitized_tx(&[&keypair0], message, Hash::default());
let instructions = vec![CompiledInstruction::new(2, &(), vec![0, 1])];
let message = Message::new_with_compiled_instructions(
@ -1982,7 +1924,7 @@ mod tests {
(message.account_keys[0], account1),
(message.account_keys[1], account2),
];
let tx1 = Transaction::new(&[&keypair1], message, Hash::default());
let tx1 = new_sanitized_tx(&[&keypair1], message, Hash::default());
let loaders = vec![(Ok(()), None), (Ok(()), None)];
@ -2026,9 +1968,9 @@ mod tests {
.unwrap()
.insert_new_readonly(&pubkey);
}
let txs = &[tx0, tx1];
let txs = vec![tx0, tx1];
let collected_accounts = accounts.collect_accounts_to_store(
txs.iter(),
&txs,
&loaders,
loaded.as_mut_slice(),
&rent_collector,
@ -2087,16 +2029,17 @@ mod tests {
}
fn load_accounts_no_store(accounts: &Accounts, tx: Transaction) -> Vec<TransactionLoadResult> {
let tx = SanitizedTransaction::try_from(tx).unwrap();
let rent_collector = RentCollector::default();
let fee_calculator = FeeCalculator::new(10);
let mut hash_queue = BlockhashQueue::new(100);
hash_queue.register_hash(&tx.message().recent_blockhash, &fee_calculator);
hash_queue.register_hash(tx.message().recent_blockhash(), &fee_calculator);
let ancestors = vec![(0, 0)].into_iter().collect();
let mut error_counters = ErrorCounters::default();
accounts.load_accounts(
&ancestors,
[tx].iter(),
&[tx],
vec![(Ok(()), None)],
&hash_queue,
&mut error_counters,
@ -2357,7 +2300,7 @@ mod tests {
(message.account_keys[3], to_account),
(message.account_keys[4], recent_blockhashes_sysvar_account),
];
let tx = Transaction::new(&[&nonce_authority, &from], message, blockhash);
let tx = new_sanitized_tx(&[&nonce_authority, &from], message, blockhash);
let nonce_state =
nonce::state::Versions::new_current(nonce::State::Initialized(nonce::state::Data {
@ -2404,9 +2347,9 @@ mod tests {
false,
AccountShrinkThreshold::default(),
);
let txs = &[tx];
let txs = vec![tx];
let collected_accounts = accounts.collect_accounts_to_store(
txs.iter(),
&txs,
&loaders,
loaded.as_mut_slice(),
&rent_collector,
@ -2475,7 +2418,7 @@ mod tests {
(message.account_keys[3], to_account),
(message.account_keys[4], recent_blockhashes_sysvar_account),
];
let tx = Transaction::new(&[&nonce_authority, &from], message, blockhash);
let tx = new_sanitized_tx(&[&nonce_authority, &from], message, blockhash);
let nonce_state =
nonce::state::Versions::new_current(nonce::State::Initialized(nonce::state::Data {
@ -2521,9 +2464,9 @@ mod tests {
false,
AccountShrinkThreshold::default(),
);
let txs = &[tx];
let txs = vec![tx];
let collected_accounts = accounts.collect_accounts_to_store(
txs.iter(),
&txs,
&loaders,
loaded.as_mut_slice(),
&rent_collector,

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ use solana_sdk::{
fee_calculator::{FeeCalculator, FeeRateGovernor},
hash::Hash,
instruction::Instruction,
message::Message,
message::{Message, SanitizedMessage},
pubkey::Pubkey,
signature::{Keypair, Signature, Signer},
signers::Signers,
@ -16,6 +16,7 @@ use solana_sdk::{
transport::{Result, TransportError},
};
use std::{
convert::TryFrom,
io,
sync::{
mpsc::{channel, Receiver, Sender},
@ -303,8 +304,9 @@ impl SyncClient for BankClient {
}
fn get_fee_for_message(&self, blockhash: &Hash, message: &Message) -> Result<u64> {
self.bank
.get_fee_for_message(blockhash, message)
SanitizedMessage::try_from(message.clone())
.ok()
.and_then(|message| self.bank.get_fee_for_message(blockhash, &message))
.ok_or_else(|| {
TransportError::IoError(io::Error::new(
io::ErrorKind::Other,

View File

@ -3,7 +3,7 @@ use crate::{
genesis_utils::{self, GenesisConfigInfo, ValidatorVoteKeypairs},
vote_sender_types::ReplayVoteSender,
};
use solana_sdk::{pubkey::Pubkey, sanitized_transaction::SanitizedTransaction, signature::Signer};
use solana_sdk::{pubkey::Pubkey, signature::Signer, transaction::SanitizedTransaction};
use solana_vote_program::vote_transaction;
pub fn setup_bank_and_vote_pubkeys_for_tests(
@ -44,8 +44,8 @@ pub fn find_and_send_votes(
assert!(execution_results[old_account.transaction_result_index]
.0
.is_ok());
let transaction = &sanitized_txs[old_account.transaction_index];
if let Some(parsed_vote) = vote_transaction::parse_vote_transaction(transaction) {
let tx = &sanitized_txs[old_account.transaction_index];
if let Some(parsed_vote) = vote_transaction::parse_sanitized_vote_transaction(tx) {
if parsed_vote.1.slots.last().is_some() {
let _ = vote_sender.send(parsed_vote);
}

View File

@ -2,7 +2,7 @@ use std::{cell::RefCell, rc::Rc};
use solana_sdk::{
instruction::{CompiledInstruction, Instruction},
message::Message,
message::SanitizedMessage,
};
/// Records and compiles cross-program invoked instructions
@ -12,11 +12,14 @@ pub struct InstructionRecorder {
}
impl InstructionRecorder {
pub fn compile_instructions(&self, message: &Message) -> Vec<CompiledInstruction> {
pub fn compile_instructions(
&self,
message: &SanitizedMessage,
) -> Option<Vec<CompiledInstruction>> {
self.inner
.borrow()
.iter()
.map(|ix| message.compile_instruction(ix))
.map(|ix| message.try_compile_instruction(ix))
.collect()
}

View File

@ -1837,8 +1837,9 @@ mod tests {
genesis_config::create_genesis_config,
signature::{Keypair, Signer},
system_transaction,
transaction::SanitizedTransaction,
};
use std::mem::size_of;
use std::{convert::TryFrom, mem::size_of};
#[test]
fn test_serialize_snapshot_data_file_under_limit() {
@ -2910,12 +2911,13 @@ mod tests {
let slot = slot + 1;
let bank2 = Arc::new(Bank::new_from_parent(&bank1, &collector, slot));
let tx = system_transaction::transfer(
let tx = SanitizedTransaction::try_from(system_transaction::transfer(
&key1,
&key2.pubkey(),
lamports_to_transfer,
bank2.last_blockhash(),
);
))
.unwrap();
let fee = bank2
.get_fee_for_message(&bank2.last_blockhash(), tx.message())
.unwrap();

View File

@ -1,16 +1,14 @@
use crate::bank::Bank;
use solana_sdk::{
sanitized_transaction::SanitizedTransaction,
transaction::{Result, Transaction},
use {
crate::bank::Bank,
solana_sdk::transaction::{Result, SanitizedTransaction},
std::borrow::Cow,
};
use std::borrow::Cow;
use std::ops::Deref;
// Represents the results of trying to lock a set of accounts
pub struct TransactionBatch<'a, 'b> {
lock_results: Vec<Result<()>>,
bank: &'a Bank,
sanitized_txs: Cow<'b, [SanitizedTransaction<'b>]>,
sanitized_txs: Cow<'b, [SanitizedTransaction]>,
pub(crate) needs_unlock: bool,
}
@ -18,7 +16,7 @@ impl<'a, 'b> TransactionBatch<'a, 'b> {
pub fn new(
lock_results: Vec<Result<()>>,
bank: &'a Bank,
sanitized_txs: Cow<'b, [SanitizedTransaction<'b>]>,
sanitized_txs: Cow<'b, [SanitizedTransaction]>,
) -> Self {
assert_eq!(lock_results.len(), sanitized_txs.len());
Self {
@ -37,10 +35,6 @@ impl<'a, 'b> TransactionBatch<'a, 'b> {
&self.sanitized_txs
}
pub fn transactions_iter(&self) -> impl Iterator<Item = &Transaction> {
self.sanitized_txs.iter().map(Deref::deref)
}
pub fn bank(&self) -> &Bank {
self.bank
}
@ -58,38 +52,33 @@ mod tests {
use super::*;
use crate::genesis_utils::{create_genesis_config_with_leader, GenesisConfigInfo};
use solana_sdk::{signature::Keypair, system_transaction};
use std::convert::TryFrom;
use std::convert::TryInto;
#[test]
fn test_transaction_batch() {
let (bank, txs) = setup();
// Test getting locked accounts
let batch = bank.prepare_batch(txs.iter()).unwrap();
let batch = bank.prepare_sanitized_batch(&txs);
// Grab locks
assert!(batch.lock_results().iter().all(|x| x.is_ok()));
// Trying to grab locks again should fail
let batch2 = bank.prepare_batch(txs.iter()).unwrap();
let batch2 = bank.prepare_sanitized_batch(&txs);
assert!(batch2.lock_results().iter().all(|x| x.is_err()));
// Drop the first set of locks
drop(batch);
// Now grabbing locks should work again
let batch2 = bank.prepare_batch(txs.iter()).unwrap();
let batch2 = bank.prepare_sanitized_batch(&txs);
assert!(batch2.lock_results().iter().all(|x| x.is_ok()));
}
#[test]
fn test_simulation_batch() {
let (bank, txs) = setup();
let txs = txs
.into_iter()
.map(SanitizedTransaction::try_from)
.collect::<Result<Vec<_>>>()
.unwrap();
// Prepare batch without locks
let batch = bank.prepare_simulation_batch(txs[0].clone());
@ -104,7 +93,7 @@ mod tests {
assert!(batch3.lock_results().iter().all(|x| x.is_ok()));
}
fn setup() -> (Bank, Vec<Transaction>) {
fn setup() -> (Bank, Vec<SanitizedTransaction>) {
let dummy_leader_pubkey = solana_sdk::pubkey::new_rand();
let GenesisConfigInfo {
genesis_config,
@ -118,8 +107,12 @@ mod tests {
let pubkey2 = solana_sdk::pubkey::new_rand();
let txs = vec![
system_transaction::transfer(&mint_keypair, &pubkey, 1, genesis_config.hash()),
system_transaction::transfer(&keypair2, &pubkey2, 1, genesis_config.hash()),
system_transaction::transfer(&mint_keypair, &pubkey, 1, genesis_config.hash())
.try_into()
.unwrap(),
system_transaction::transfer(&keypair2, &pubkey2, 1, genesis_config.hash())
.try_into()
.unwrap(),
];
(bank, txs)