Add simulation detection countermeasure (#22880)
* Add simulation detection countermeasures * Add program and test using TestValidator * Remove incinerator deposit * Remove incinerator * Update Cargo.lock * Add more features to simulation bank * Update Cargo.lock per rebase Co-authored-by: Jon Cinque <jon.cinque@gmail.com>
This commit is contained in:
parent
d2a407a9a7
commit
c42b80f099
|
@ -1587,7 +1587,10 @@ impl ReplayStage {
|
||||||
root_slot,
|
root_slot,
|
||||||
my_pubkey,
|
my_pubkey,
|
||||||
rpc_subscriptions,
|
rpc_subscriptions,
|
||||||
NewBankOptions { vote_only_bank },
|
NewBankOptions {
|
||||||
|
vote_only_bank,
|
||||||
|
simulation_bank: false,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let tpu_bank = bank_forks.write().unwrap().insert(tpu_bank);
|
let tpu_bank = bank_forks.write().unwrap().insert(tpu_bank);
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -26,6 +26,7 @@ itertools = "0.10.1"
|
||||||
log = "0.4.11"
|
log = "0.4.11"
|
||||||
miow = "0.3.6"
|
miow = "0.3.6"
|
||||||
net2 = "0.2.37"
|
net2 = "0.2.37"
|
||||||
|
solana-account-decoder = { path = "../../account-decoder", version = "=1.10.0" }
|
||||||
solana-bpf-rust-invoke = { path = "rust/invoke", version = "=1.10.0"}
|
solana-bpf-rust-invoke = { path = "rust/invoke", version = "=1.10.0"}
|
||||||
solana-bpf-loader-program = { path = "../bpf_loader", version = "=1.10.0"}
|
solana-bpf-loader-program = { path = "../bpf_loader", version = "=1.10.0"}
|
||||||
solana-bpf-rust-realloc = { path = "rust/realloc", version = "=1.10.0"}
|
solana-bpf-rust-realloc = { path = "rust/realloc", version = "=1.10.0"}
|
||||||
|
@ -38,7 +39,6 @@ solana-runtime = { path = "../../runtime", version = "=1.10.0" }
|
||||||
solana-program-runtime = { path = "../../program-runtime", version = "=1.10.0" }
|
solana-program-runtime = { path = "../../program-runtime", version = "=1.10.0" }
|
||||||
solana-sdk = { path = "../../sdk", version = "=1.10.0" }
|
solana-sdk = { path = "../../sdk", version = "=1.10.0" }
|
||||||
solana-transaction-status = { path = "../../transaction-status", version = "=1.10.0" }
|
solana-transaction-status = { path = "../../transaction-status", version = "=1.10.0" }
|
||||||
solana-account-decoder = { path = "../../account-decoder", version = "=1.10.0" }
|
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "bpf_loader"
|
name = "bpf_loader"
|
||||||
|
@ -83,6 +83,7 @@ members = [
|
||||||
"rust/sha",
|
"rust/sha",
|
||||||
"rust/sibling_inner_instruction",
|
"rust/sibling_inner_instruction",
|
||||||
"rust/sibling_instruction",
|
"rust/sibling_instruction",
|
||||||
|
"rust/simulation",
|
||||||
"rust/spoof1",
|
"rust/spoof1",
|
||||||
"rust/spoof1_system",
|
"rust/spoof1_system",
|
||||||
"rust/sysvar",
|
"rust/sysvar",
|
||||||
|
|
|
@ -93,6 +93,7 @@ fn main() {
|
||||||
"sha",
|
"sha",
|
||||||
"sibling_inner_instruction",
|
"sibling_inner_instruction",
|
||||||
"sibling_instruction",
|
"sibling_instruction",
|
||||||
|
"simulation",
|
||||||
"spoof1",
|
"spoof1",
|
||||||
"spoof1_system",
|
"spoof1_system",
|
||||||
"upgradeable",
|
"upgradeable",
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
[package]
|
||||||
|
name = "solana-bpf-rust-simulation"
|
||||||
|
version = "1.10.0"
|
||||||
|
description = "Solana BPF Program Simulation Differences"
|
||||||
|
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||||
|
repository = "https://github.com/solana-labs/solana"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
homepage = "https://solana.com/"
|
||||||
|
documentation = "https://docs.rs/solana-bpf-rust-simulation"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-bpf = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
solana-program = { path = "../../../../sdk/program", version = "=1.10.0" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
solana-logger = { path = "../../../../logger", version = "=1.10.0" }
|
||||||
|
solana-program-test = { path = "../../../../program-test", version = "=1.10.0" }
|
||||||
|
solana-sdk = { path = "../../../../sdk", version = "=1.10.0" }
|
||||||
|
solana-validator = { path = "../../../../validator", version = "=1.10.0" }
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "lib"]
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
targets = ["x86_64-unknown-linux-gnu"]
|
|
@ -0,0 +1,39 @@
|
||||||
|
use solana_program::{
|
||||||
|
account_info::{next_account_info, AccountInfo},
|
||||||
|
clock::Clock,
|
||||||
|
declare_id, entrypoint,
|
||||||
|
entrypoint::ProgramResult,
|
||||||
|
msg,
|
||||||
|
pubkey::Pubkey,
|
||||||
|
sysvar::Sysvar,
|
||||||
|
};
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
declare_id!("Sim1jD5C35odT8mzctm8BWnjic8xW5xgeb5MbcbErTo");
|
||||||
|
|
||||||
|
entrypoint!(process_instruction);
|
||||||
|
|
||||||
|
pub fn process_instruction(
|
||||||
|
_program_id: &Pubkey,
|
||||||
|
accounts: &[AccountInfo],
|
||||||
|
_instruction_data: &[u8],
|
||||||
|
) -> ProgramResult {
|
||||||
|
let account_info_iter = &mut accounts.iter();
|
||||||
|
let slot_account = next_account_info(account_info_iter)?;
|
||||||
|
|
||||||
|
// Slot is an u64 at the end of the structure
|
||||||
|
let data = slot_account.data.borrow();
|
||||||
|
let slot: u64 = u64::from_le_bytes(data[data.len() - 8..].try_into().unwrap());
|
||||||
|
|
||||||
|
let clock = Clock::get().unwrap();
|
||||||
|
|
||||||
|
msg!("next_slot is {:?} ", slot);
|
||||||
|
msg!("clock is in slot {:?} ", clock.slot);
|
||||||
|
if clock.slot >= slot {
|
||||||
|
msg!("On-chain");
|
||||||
|
} else {
|
||||||
|
panic!("Simulation");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
#![cfg(feature = "test-bpf")]
|
||||||
|
|
||||||
|
use {
|
||||||
|
solana_bpf_rust_simulation::process_instruction,
|
||||||
|
solana_program_test::{processor, tokio, ProgramTest},
|
||||||
|
solana_sdk::{
|
||||||
|
instruction::{AccountMeta, Instruction},
|
||||||
|
pubkey::Pubkey,
|
||||||
|
signature::Signer,
|
||||||
|
sysvar,
|
||||||
|
transaction::Transaction,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn no_panic() {
|
||||||
|
let program_id = Pubkey::new_unique();
|
||||||
|
let program_test = ProgramTest::new(
|
||||||
|
"solana_bpf_rust_simulation",
|
||||||
|
program_id,
|
||||||
|
processor!(process_instruction),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut context = program_test.start_with_context().await;
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[Instruction {
|
||||||
|
program_id,
|
||||||
|
accounts: vec![AccountMeta::new_readonly(sysvar::slot_history::id(), false)],
|
||||||
|
data: vec![],
|
||||||
|
}],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer],
|
||||||
|
context.last_blockhash,
|
||||||
|
);
|
||||||
|
|
||||||
|
context
|
||||||
|
.banks_client
|
||||||
|
.process_transaction_with_preflight(transaction)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
#![cfg(feature = "test-bpf")]
|
||||||
|
|
||||||
|
use {
|
||||||
|
solana_program::{
|
||||||
|
instruction::{AccountMeta, Instruction},
|
||||||
|
pubkey::Pubkey,
|
||||||
|
sysvar,
|
||||||
|
},
|
||||||
|
solana_sdk::{signature::Signer, transaction::Transaction},
|
||||||
|
solana_validator::test_validator::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_panic() {
|
||||||
|
solana_logger::setup_with_default("solana_program_runtime=debug");
|
||||||
|
let program_id = Pubkey::new_unique();
|
||||||
|
|
||||||
|
let (test_validator, payer) = TestValidatorGenesis::default()
|
||||||
|
.add_program("solana_bpf_rust_simulation", program_id)
|
||||||
|
.start();
|
||||||
|
let rpc_client = test_validator.get_rpc_client();
|
||||||
|
let blockhash = rpc_client.get_latest_blockhash().unwrap();
|
||||||
|
|
||||||
|
let transaction = Transaction::new_signed_with_payer(
|
||||||
|
&[Instruction {
|
||||||
|
program_id,
|
||||||
|
accounts: vec![AccountMeta::new_readonly(sysvar::slot_history::id(), false)],
|
||||||
|
data: vec![],
|
||||||
|
}],
|
||||||
|
Some(&payer.pubkey()),
|
||||||
|
&[&payer],
|
||||||
|
blockhash,
|
||||||
|
);
|
||||||
|
|
||||||
|
rpc_client
|
||||||
|
.send_and_confirm_transaction(&transaction)
|
||||||
|
.unwrap();
|
||||||
|
}
|
|
@ -3472,7 +3472,7 @@ pub mod rpc_full {
|
||||||
let preflight_commitment = config
|
let preflight_commitment = config
|
||||||
.preflight_commitment
|
.preflight_commitment
|
||||||
.map(|commitment| CommitmentConfig { commitment });
|
.map(|commitment| CommitmentConfig { commitment });
|
||||||
let preflight_bank = &*meta.bank(preflight_commitment);
|
let preflight_bank = meta.bank(preflight_commitment);
|
||||||
let transaction = sanitize_transaction(unsanitized_tx)?;
|
let transaction = sanitize_transaction(unsanitized_tx)?;
|
||||||
let signature = *transaction.signature();
|
let signature = *transaction.signature();
|
||||||
|
|
||||||
|
@ -3569,7 +3569,7 @@ pub mod rpc_full {
|
||||||
let (_, mut unsanitized_tx) =
|
let (_, mut unsanitized_tx) =
|
||||||
decode_and_deserialize::<VersionedTransaction>(data, encoding)?;
|
decode_and_deserialize::<VersionedTransaction>(data, encoding)?;
|
||||||
|
|
||||||
let bank = &*meta.bank(config.commitment);
|
let bank = meta.bank(config.commitment);
|
||||||
if config.replace_recent_blockhash {
|
if config.replace_recent_blockhash {
|
||||||
if config.sig_verify {
|
if config.sig_verify {
|
||||||
return Err(Error::invalid_params(
|
return Err(Error::invalid_params(
|
||||||
|
@ -3637,7 +3637,7 @@ pub mod rpc_full {
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(new_response(
|
Ok(new_response(
|
||||||
bank,
|
&bank,
|
||||||
RpcSimulateTransactionResult {
|
RpcSimulateTransactionResult {
|
||||||
err: result.err(),
|
err: result.err(),
|
||||||
logs: Some(logs),
|
logs: Some(logs),
|
||||||
|
|
|
@ -199,9 +199,16 @@ impl Accounts {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_from_parent(parent: &Accounts, slot: Slot, parent_slot: Slot) -> Self {
|
pub fn new_from_parent(
|
||||||
|
parent: &Accounts,
|
||||||
|
slot: Slot,
|
||||||
|
parent_slot: Slot,
|
||||||
|
simulation_bank: bool,
|
||||||
|
) -> Self {
|
||||||
let accounts_db = parent.accounts_db.clone();
|
let accounts_db = parent.accounts_db.clone();
|
||||||
accounts_db.set_hash(slot, parent_slot);
|
if !simulation_bank {
|
||||||
|
accounts_db.set_hash(slot, parent_slot);
|
||||||
|
}
|
||||||
Self {
|
Self {
|
||||||
accounts_db,
|
accounts_db,
|
||||||
account_locks: Mutex::new(AccountLocks::default()),
|
account_locks: Mutex::new(AccountLocks::default()),
|
||||||
|
|
|
@ -1035,6 +1035,18 @@ pub trait DropCallback: fmt::Debug {
|
||||||
fn clone_box(&self) -> Box<dyn DropCallback + Send + Sync>;
|
fn clone_box(&self) -> Box<dyn DropCallback + Send + Sync>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Noop callback on dropping banks is useful for simulation banks, which are
|
||||||
|
/// new banks created from a frozen bank, but should not be purged in the same
|
||||||
|
/// way.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct NoopDropCallback;
|
||||||
|
impl DropCallback for NoopDropCallback {
|
||||||
|
fn callback(&self, _b: &Bank) {}
|
||||||
|
fn clone_box(&self) -> Box<dyn DropCallback + Send + Sync> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Serialize, Deserialize, AbiExample, Clone, Copy)]
|
#[derive(Debug, PartialEq, Serialize, Deserialize, AbiExample, Clone, Copy)]
|
||||||
pub struct RewardInfo {
|
pub struct RewardInfo {
|
||||||
pub reward_type: RewardType,
|
pub reward_type: RewardType,
|
||||||
|
@ -1253,6 +1265,7 @@ struct LoadVoteAndStakeAccountsResult {
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct NewBankOptions {
|
pub struct NewBankOptions {
|
||||||
pub vote_only_bank: bool,
|
pub vote_only_bank: bool,
|
||||||
|
pub simulation_bank: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Bank {
|
impl Bank {
|
||||||
|
@ -1527,7 +1540,10 @@ impl Bank {
|
||||||
new_bank_options: NewBankOptions,
|
new_bank_options: NewBankOptions,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut time = Measure::start("bank::new_from_parent");
|
let mut time = Measure::start("bank::new_from_parent");
|
||||||
let NewBankOptions { vote_only_bank } = new_bank_options;
|
let NewBankOptions {
|
||||||
|
vote_only_bank,
|
||||||
|
simulation_bank,
|
||||||
|
} = new_bank_options;
|
||||||
|
|
||||||
parent.freeze();
|
parent.freeze();
|
||||||
assert_ne!(slot, parent.slot());
|
assert_ne!(slot, parent.slot());
|
||||||
|
@ -1541,6 +1557,7 @@ impl Bank {
|
||||||
&parent.rc.accounts,
|
&parent.rc.accounts,
|
||||||
slot,
|
slot,
|
||||||
parent.slot(),
|
parent.slot(),
|
||||||
|
simulation_bank,
|
||||||
)),
|
)),
|
||||||
parent: RwLock::new(Some(parent.clone())),
|
parent: RwLock::new(Some(parent.clone())),
|
||||||
slot,
|
slot,
|
||||||
|
@ -1630,6 +1647,20 @@ impl Bank {
|
||||||
let (feature_set, feature_set_time) =
|
let (feature_set, feature_set_time) =
|
||||||
Measure::this(|_| parent.feature_set.clone(), (), "feature_set_creation");
|
Measure::this(|_| parent.feature_set.clone(), (), "feature_set_creation");
|
||||||
|
|
||||||
|
let drop_callback = if simulation_bank {
|
||||||
|
RwLock::new(OptionalDropCallback(Some(Box::new(NoopDropCallback))))
|
||||||
|
} else {
|
||||||
|
RwLock::new(OptionalDropCallback(
|
||||||
|
parent
|
||||||
|
.drop_callback
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.as_ref()
|
||||||
|
.map(|drop_callback| drop_callback.clone_box()),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
|
||||||
let mut new = Bank {
|
let mut new = Bank {
|
||||||
rc,
|
rc,
|
||||||
src,
|
src,
|
||||||
|
@ -1683,15 +1714,7 @@ impl Bank {
|
||||||
transaction_log_collector_config,
|
transaction_log_collector_config,
|
||||||
transaction_log_collector: Arc::new(RwLock::new(TransactionLogCollector::default())),
|
transaction_log_collector: Arc::new(RwLock::new(TransactionLogCollector::default())),
|
||||||
feature_set,
|
feature_set,
|
||||||
drop_callback: RwLock::new(OptionalDropCallback(
|
drop_callback,
|
||||||
parent
|
|
||||||
.drop_callback
|
|
||||||
.read()
|
|
||||||
.unwrap()
|
|
||||||
.0
|
|
||||||
.as_ref()
|
|
||||||
.map(|drop_callback| drop_callback.clone_box()),
|
|
||||||
)),
|
|
||||||
freeze_started: AtomicBool::new(false),
|
freeze_started: AtomicBool::new(false),
|
||||||
cost_tracker: RwLock::new(CostTracker::default()),
|
cost_tracker: RwLock::new(CostTracker::default()),
|
||||||
sysvar_cache: RwLock::new(SysvarCache::default()),
|
sysvar_cache: RwLock::new(SysvarCache::default()),
|
||||||
|
@ -3472,12 +3495,26 @@ 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(
|
pub fn simulate_transaction(
|
||||||
&self,
|
self: &Arc<Bank>,
|
||||||
transaction: SanitizedTransaction,
|
transaction: SanitizedTransaction,
|
||||||
) -> TransactionSimulationResult {
|
) -> TransactionSimulationResult {
|
||||||
assert!(self.is_frozen(), "simulation bank must be frozen");
|
assert!(self.is_frozen(), "simulation bank must be frozen");
|
||||||
|
|
||||||
self.simulate_transaction_unchecked(transaction)
|
// Simulation detection countermeasure 1: Create a new child bank for the simulation. This
|
||||||
|
// ensures comparing the slot values between the Clock and SlotHistory sysvars does not
|
||||||
|
// reveal that the program is running in simulation.
|
||||||
|
//
|
||||||
|
// Reference: https://opcodes.fr/en/publications/2022-01/detecting-transaction-simulation/
|
||||||
|
let bank = Bank::new_from_parent_with_options(
|
||||||
|
self,
|
||||||
|
&Pubkey::default(),
|
||||||
|
self.slot().saturating_add(1),
|
||||||
|
NewBankOptions {
|
||||||
|
simulation_bank: true,
|
||||||
|
..NewBankOptions::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
bank.simulate_transaction_unchecked(transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run transactions against a bank without committing the results; does not check if the bank
|
/// Run transactions against a bank without committing the results; does not check if the bank
|
||||||
|
|
Loading…
Reference in New Issue