use std::rc::Rc;
use std::thread;
use log::{info, warn};
use soroban_env_host::budget::Budget;
use soroban_env_host::e2e_invoke::{
invoke_host_function_in_recording_mode, RecordingInvocationAuthMode,
};
use soroban_env_host::storage::SnapshotSource;
use soroban_env_host::xdr::{
AccountId, ContractEvent, DiagnosticEvent, HostFunction, LedgerEntry, LedgerKey, ScVal,
SorobanAuthorizationEntry, SorobanResources,
};
use tokio::sync::{mpsc, oneshot};
use crate::{ForkConfig, ForkError};
#[derive(Debug)]
pub(crate) enum Command {
GetNetwork {
reply: oneshot::Sender<NetworkReply>,
},
GetLatestLedger {
reply: oneshot::Sender<LatestLedgerReply>,
},
GetLedgersPage {
reply: oneshot::Sender<LedgersPageReply>,
},
GetLedgerEntries {
keys: Vec<LedgerKey>,
reply: oneshot::Sender<LedgerEntriesReply>,
},
SimulateTransaction {
host_function: HostFunction,
source_account: AccountId,
reply: oneshot::Sender<SimulationReply>,
},
}
#[derive(Debug)]
pub(crate) struct NetworkReply {
pub(crate) passphrase: String,
pub(crate) protocol_version: u32,
pub(crate) network_id_hex: String,
}
#[derive(Debug)]
pub(crate) struct LatestLedgerReply {
pub(crate) sequence: u32,
pub(crate) protocol_version: u32,
pub(crate) id: String,
}
#[derive(Debug)]
pub(crate) struct LedgersPageReply {
pub(crate) sequence: u32,
pub(crate) close_time: u64,
}
#[derive(Debug)]
pub(crate) struct LedgerEntriesReply {
pub(crate) entries: Vec<Option<(LedgerKey, LedgerEntry, Option<u32>)>>,
pub(crate) latest_ledger: u32,
}
#[derive(Debug)]
pub(crate) struct SimulationReply {
pub(crate) result: std::result::Result<ScVal, String>,
pub(crate) auth: Vec<SorobanAuthorizationEntry>,
pub(crate) resources: SorobanResources,
pub(crate) contract_events: Vec<ContractEvent>,
pub(crate) diagnostic_events: Vec<DiagnosticEvent>,
pub(crate) latest_ledger: u32,
}
#[derive(Clone)]
pub(crate) struct ActorHandle {
tx: mpsc::Sender<Command>,
}
impl ActorHandle {
pub(crate) async fn send<R>(
&self,
build: impl FnOnce(oneshot::Sender<R>) -> Command,
) -> Result<R, ActorError> {
let (reply_tx, reply_rx) = oneshot::channel();
let cmd = build(reply_tx);
self.tx
.send(cmd)
.await
.map_err(|_| ActorError::WorkerGone)?;
reply_rx.await.map_err(|_| ActorError::WorkerGone)
}
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum ActorError {
#[error("worker thread is no longer running")]
WorkerGone,
}
pub(crate) fn spawn(
config: ForkConfig,
) -> (
ActorHandle,
oneshot::Receiver<std::result::Result<(), ForkError>>,
) {
let (tx, rx) = mpsc::channel(32);
let (ready_tx, ready_rx) = oneshot::channel();
thread::Builder::new()
.name("soroban-fork-worker".into())
.spawn(move || {
let env = match config.build() {
Ok(env) => {
let _ = ready_tx.send(Ok(()));
env
}
Err(e) => {
let _ = ready_tx.send(Err(e));
return;
}
};
worker_loop(env, rx);
})
.expect("spawn soroban-fork-worker thread");
(ActorHandle { tx }, ready_rx)
}
fn worker_loop(env: crate::ForkedEnv, mut rx: mpsc::Receiver<Command>) {
info!("soroban-fork: worker thread started");
while let Some(cmd) = rx.blocking_recv() {
match cmd {
Command::GetNetwork { reply } => {
let passphrase = env
.passphrase()
.map(|s| s.to_string())
.unwrap_or_else(|| "Forked Soroban Network (custom network_id)".to_string());
let _ = reply.send(NetworkReply {
passphrase,
protocol_version: env.protocol_version(),
network_id_hex: hex_encode(&env.network_id()),
});
}
Command::GetLatestLedger { reply } => {
let _ = reply.send(LatestLedgerReply {
sequence: env.ledger_sequence(),
protocol_version: env.protocol_version(),
id: format!("forked-ledger-{}", env.ledger_sequence()),
});
}
Command::GetLedgersPage { reply } => {
let _ = reply.send(LedgersPageReply {
sequence: env.ledger_sequence(),
close_time: env.ledger_close_time(),
});
}
Command::GetLedgerEntries { keys, reply } => {
let entries = resolve_ledger_entries(&env, keys);
let _ = reply.send(LedgerEntriesReply {
entries,
latest_ledger: env.ledger_sequence(),
});
}
Command::SimulateTransaction {
host_function,
source_account,
reply,
} => {
let _ = reply.send(simulate_transaction(&env, host_function, source_account));
}
}
}
warn!("soroban-fork: worker thread shutting down (channel closed)");
drop(env);
info!("soroban-fork: worker thread exited");
}
fn simulate_transaction(
env: &crate::ForkedEnv,
host_function: HostFunction,
source_account: AccountId,
) -> SimulationReply {
use soroban_sdk::testutils::Ledger as _;
let snapshot_source: Rc<dyn SnapshotSource> = env.snapshot_source().clone();
let ledger_info = env.env().ledger().get();
let budget = Budget::default();
let auth_mode = RecordingInvocationAuthMode::Recording(false);
let mut diagnostic_events: Vec<DiagnosticEvent> = Vec::new();
let result = invoke_host_function_in_recording_mode(
&budget,
true, &host_function,
&source_account,
auth_mode,
ledger_info,
snapshot_source,
[0u8; 32], &mut diagnostic_events,
);
let latest_ledger = env.ledger_sequence();
match result {
Ok(rec) => SimulationReply {
result: rec.invoke_result.map_err(|e| format!("host error: {e}")),
auth: rec.auth,
resources: rec.resources,
contract_events: rec.contract_events,
diagnostic_events,
latest_ledger,
},
Err(e) => {
SimulationReply {
result: Err(format!("recording-mode error: {e}")),
auth: Vec::new(),
resources: empty_soroban_resources(),
contract_events: Vec::new(),
diagnostic_events,
latest_ledger,
}
}
}
}
fn empty_soroban_resources() -> SorobanResources {
use soroban_env_host::xdr::LedgerFootprint;
SorobanResources {
footprint: LedgerFootprint {
read_only: vec![].try_into().expect("empty vec into VecM"),
read_write: vec![].try_into().expect("empty vec into VecM"),
},
instructions: 0,
disk_read_bytes: 0,
write_bytes: 0,
}
}
fn resolve_ledger_entries(
env: &crate::ForkedEnv,
keys: Vec<LedgerKey>,
) -> Vec<Option<(LedgerKey, LedgerEntry, Option<u32>)>> {
let source = env.snapshot_source();
keys.into_iter()
.map(|key| {
let key_rc = Rc::new(key.clone());
match source.get(&key_rc) {
Ok(Some((entry_rc, live_until))) => {
Some((key, entry_rc.as_ref().clone(), live_until))
}
Ok(None) => None,
Err(_) => None,
}
})
.collect()
}
fn hex_encode(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{b:02x}"));
}
s
}