#![cfg_attr(not(target_family = "wasm"), allow(unused_imports, dead_code))]
use crate::{
actions::login_action, check, create_boot_transactions, get_optional_result_bytes,
get_result_bytes, services, status_key, tester_raw, AccountNumber, Action, BlockTime, Caller,
Checksum256, CodeByHashRow, CodeRow, DbId, DirectoryRegistry, Error, HostConfigRow, HttpBody,
HttpHeader, HttpReply, HttpRequest, InnerTraceEnum, PackageRegistry, RunMode, Seconds,
SignedTransaction, StatusRow, Tapos, TimePointSec, TimePointUSec, ToKey, Transaction,
TransactionTrace,
};
#[cfg(target_family = "wasm")]
use crate::{MicroSeconds, PackageList, PackageOp};
use anyhow::anyhow;
use chrono::Utc;
use fracpack::{Pack, Unpack, UnpackOwned};
use futures::executor::block_on;
use psibase_macros::account_raw;
use serde::{de::DeserializeOwned, Deserialize};
use sha2::{Digest, Sha256};
use std::cell::{Cell, RefCell};
use std::path::{Path, PathBuf};
use std::{marker::PhantomData, ptr::null_mut};
pub fn execute(command: &str) -> i32 {
unsafe { tester_raw::testerExecute(command.as_ptr(), command.len()) }
}
#[derive(Debug)]
pub struct Chain {
chain_handle: u32,
status: RefCell<Option<StatusRow>>,
producing: Cell<bool>,
is_auto_block_start: bool,
}
pub const PRODUCER_ACCOUNT: AccountNumber = AccountNumber::new(account_raw!("prod"));
impl Default for Chain {
fn default() -> Self {
Self::new()
}
}
impl Drop for Chain {
fn drop(&mut self) {
unsafe { tester_raw::destroyChain(self.chain_handle) }
tester_raw::tester_clear_chain_for_db(self.chain_handle)
}
}
#[cfg(not(target_family = "wasm"))]
impl Chain {
pub fn new() -> Chain {
unimplemented!();
}
pub fn push(&self, _transaction: &SignedTransaction) -> TransactionTrace {
unimplemented!();
}
pub fn start_block(&self) {
unimplemented!();
}
}
#[cfg(target_family = "wasm")]
impl Chain {
pub fn new() -> Chain {
Self::create(1 << 27, 1 << 27, 1 << 27, 1 << 27)
}
pub fn boot(&self) -> Result<(), Error> {
let default_services: Vec<String> = vec!["TestDefault".to_string()];
self.boot_with(&Self::default_registry(), &default_services[..])
}
pub fn default_registry() -> DirectoryRegistry {
let psibase_data_dir = std::env::var("PSIBASE_DATADIR")
.expect("Cannot find package directory: PSIBASE_DATADIR not defined");
let packages_dir = Path::new(&psibase_data_dir).join("packages");
DirectoryRegistry::new(packages_dir)
}
pub fn boot_with<R: PackageRegistry>(&self, reg: &R, services: &[String]) -> Result<(), Error> {
let mut services = block_on(reg.resolve(services))?;
const COMPRESSION_LEVEL: u32 = 4;
let (boot_tx, subsequent_tx) = create_boot_transactions(
&None,
&None,
PRODUCER_ACCOUNT,
false,
TimePointSec { seconds: 10 },
&mut services[..],
COMPRESSION_LEVEL,
)
.unwrap();
for trx in boot_tx {
self.push(&trx).ok()?;
}
self.start_block();
for (_, group, _) in subsequent_tx {
for trx in group {
self.push(&trx).ok()?;
}
}
self.start_block();
Ok(())
}
pub fn create(hot_bytes: u64, warm_bytes: u64, cool_bytes: u64, cold_bytes: u64) -> Chain {
println!("TESTER CREATE");
let chain_handle =
unsafe { tester_raw::createChain(hot_bytes, warm_bytes, cool_bytes, cold_bytes) };
if chain_handle == 0 {
tester_raw::tester_select_chain_for_db(chain_handle)
}
let mut result = Chain {
chain_handle,
status: None.into(),
producing: false.into(),
is_auto_block_start: true,
};
result.load_local_services();
result.start_session();
result
}
fn load_local_services(&mut self) {
use crate::{CODE_TABLE, NATIVE_TABLE_PRIMARY_INDEX};
let prefix = (CODE_TABLE, NATIVE_TABLE_PRIMARY_INDEX).to_key();
if unsafe {
tester_raw::kvGreaterEqual(
self.chain_handle,
DbId::NativeSubjective,
prefix.as_ptr(),
prefix.len() as u32,
prefix.len() as u32,
)
} != u32::MAX
{
return;
}
let packages_root: PathBuf = std::env::var_os("PSIBASE_DATADIR")
.expect("Cannot find local service directory")
.into();
let packages_dir = packages_root.join("packages");
let registry = DirectoryRegistry::new(packages_dir);
let package_names = vec!["XDefault".to_string()];
let packages =
block_on(PackageList::new().resolve_changes(®istry, &package_names, false, true))
.unwrap();
let mut requests = Vec::new();
unsafe {
tester_raw::checkoutSubjective(self.chain_handle);
}
for op in packages {
let PackageOp::Install(info) = op else {
panic!("Only install is expected when there are no existing packages")
};
let mut package = block_on(registry.get_by_info(&info)).unwrap();
for (account, info, code) in package.services() {
let hash: [u8; 32] = Sha256::digest(&code).into();
let code_hash: Checksum256 = hash.into();
let code_row = CodeRow {
codeNum: *account,
flags: info.parse_flags(),
codeHash: code_hash.clone(),
vmType: 0,
vmVersion: 0,
};
let key = code_row.key();
self.kv_put(DbId::NativeSubjective, &key, &code_row);
let code_by_hash_row = CodeByHashRow {
codeHash: code_hash,
vmType: 0,
vmVersion: 0,
code: code.into(),
};
let key = code_by_hash_row.key();
self.kv_put(DbId::NativeSubjective, &key, &code_by_hash_row);
}
let root_host = "\0";
for (account, path, file) in package.data() {
let Some(mime_type) = mime_guess::from_path(&path).first() else {
panic!("Cannot determine Mime-Type for {}", path)
};
requests.push(HttpRequest {
host: account.to_string() + "." + root_host,
method: "PUT".to_string(),
target: path,
contentType: mime_type.to_string(),
headers: Vec::new(),
body: file.into(),
});
}
requests.push(HttpRequest {
host: services::x_packages::SERVICE.to_string() + "." + root_host,
method: "PUT".to_string(),
target: format!("/manifest/{}", info.sha256),
contentType: "application/json".to_string(),
headers: vec![],
body: serde_json::to_string(&package.manifest())
.unwrap()
.into_bytes()
.into(),
});
requests.push(HttpRequest {
host: services::x_packages::SERVICE.to_string() + "." + root_host,
method: "POST".to_string(),
target: "/postinstall".to_string(),
contentType: "application/json".to_string(),
headers: vec![],
body: serde_json::to_string(&info).unwrap().into_bytes().into(),
});
}
check(
unsafe { tester_raw::commitSubjective(self.chain_handle) },
"Failed to commit changes",
);
for request in requests {
let reply = self.http(&request).unwrap();
if reply.status != 200 {
panic!("PUT failed: {}", reply.text().unwrap());
}
}
}
fn start_session(&mut self) {
let row = HostConfigRow {
hostVersion: format!("psitest-{}", env!("CARGO_PKG_VERSION")),
config: r#"{"host":{"hosts":["psibase.io"]}}"#.to_string(),
};
unsafe {
tester_raw::checkoutSubjective(self.chain_handle);
}
self.kv_put(DbId::NativeSession, &row.key(), &row);
check(
unsafe { tester_raw::commitSubjective(self.chain_handle) },
"Failed to commit changes",
);
let trace = self.run_action(
RunMode::RPC,
true,
services::x_http::Wrapper::pack_from(AccountNumber::default()).startSession(),
);
ChainEmptyResult { trace }.get().unwrap()
}
pub fn start_block_after(&self, micro_seconds: MicroSeconds) {
let current_time = {
let status = self.status.borrow();
status
.as_ref()
.map(|s| s.current.time)
.unwrap_or(TimePointUSec { microseconds: 0 })
};
self.start_block_at(current_time + micro_seconds);
}
pub fn start_block_at(&self, time: BlockTime) {
let status = &mut *self.status.borrow_mut();
let (producer, term, mut commit_num) = if let Some(status) = status {
let producers = if let Some(next) = &status.consensus.next {
if status.current.commitNum < next.blockNum {
status.consensus.current.data.producers()
} else {
next.consensus.data.producers()
}
} else {
status.consensus.current.data.producers()
};
(
if producers.is_empty() {
AccountNumber::from("firstproducer")
} else {
producers[0].name
},
status.current.term,
status.head.as_ref().map_or(0, |head| head.header.blockNum),
)
} else {
(AccountNumber::from("firstproducer"), 0, 0)
};
if let Some(status) = status {
if status.current.time + Seconds::new(1) < time {
unsafe {
tester_raw::startBlock(
self.chain_handle,
(time - Seconds::new(1)).microseconds,
producer.value,
term,
commit_num,
)
}
commit_num += 1;
}
}
unsafe {
tester_raw::startBlock(
self.chain_handle,
time.microseconds,
producer.value,
term,
commit_num,
)
}
*status = self
.kv_get::<StatusRow, _>(StatusRow::DB, &status_key())
.unwrap();
self.producing.replace(true);
}
pub fn start_block(&self) {
self.start_block_after(Seconds::new(1).into())
}
pub fn finish_block(&self) {
unsafe { tester_raw::finishBlock(self.chain_handle) }
self.producing.replace(false);
}
pub fn set_auto_block_start(&mut self, enable: bool) {
self.is_auto_block_start = enable;
}
pub fn push(&self, transaction: &SignedTransaction) -> TransactionTrace {
if !self.producing.get() {
self.start_block();
}
let transaction = transaction.packed();
let size = unsafe {
tester_raw::pushTransaction(self.chain_handle, transaction.as_ptr(), transaction.len())
};
TransactionTrace::unpacked(&get_result_bytes(size)).unwrap()
}
pub fn run_action(&mut self, mode: RunMode, head: bool, action: Action) -> TransactionTrace {
let packed = action.packed();
let size = unsafe {
tester_raw::runAction(self.chain_handle, mode, head, packed.as_ptr(), packed.len())
};
TransactionTrace::unpacked(&get_result_bytes(size)).unwrap()
}
pub fn copy_database(&self, path: &str) -> i32 {
let src = unsafe { self.get_path() };
execute(&format! {"mkdir -p {path} && cp -a {src}/* {path}", src = src, path = path})
}
pub unsafe fn get_path(&self) -> String {
let size = tester_raw::getChainPath(self.chain_handle, null_mut(), 0);
let mut bytes = Vec::with_capacity(size);
tester_raw::getChainPath(self.chain_handle, bytes.as_mut_ptr(), size);
bytes.set_len(size);
String::from_utf8_unchecked(bytes)
}
pub fn select_chain(&self) {
tester_raw::tester_select_chain_for_db(self.chain_handle)
}
pub fn kv_get_bytes(&self, db: DbId, key: &[u8]) -> Option<Vec<u8>> {
let size =
unsafe { tester_raw::kvGet(self.chain_handle, db, key.as_ptr(), key.len() as u32) };
get_optional_result_bytes(size)
}
pub fn kv_get<V: UnpackOwned, K: ToKey>(
&self,
db: DbId,
key: &K,
) -> Result<Option<V>, fracpack::Error> {
if let Some(v) = self.kv_get_bytes(db, &key.to_key()) {
Ok(Some(V::unpacked(&v)?))
} else {
Ok(None)
}
}
pub fn kv_put_bytes(&mut self, db: DbId, key: &[u8], value: &[u8]) {
unsafe {
tester_raw::kvPut(
self.chain_handle,
db,
key.as_ptr(),
key.len() as u32,
value.as_ptr(),
value.len() as u32,
)
}
}
pub fn kv_put<K: ToKey, V: Pack>(&mut self, db: DbId, key: &K, value: &V) {
self.kv_put_bytes(db, &key.to_key(), &value.packed())
}
pub fn new_account(&self, account: AccountNumber) -> Result<(), anyhow::Error> {
services::accounts::Wrapper::push(self)
.newAccount(account, AccountNumber::new(account_raw!("auth-any")), false)
.get()
}
pub fn deploy_service(&self, account: AccountNumber, code: &[u8]) -> Result<(), anyhow::Error> {
self.new_account(account)?;
services::setcode::Wrapper::push_from(self, account)
.setCode(account, 0, 0, code.to_vec().into())
.get()
}
pub fn http(&self, request: &HttpRequest) -> Result<HttpReply, anyhow::Error> {
let packed_request = request.packed();
let fd = unsafe {
tester_raw::httpRequest(
self.chain_handle,
packed_request.as_ptr(),
packed_request.len(),
)
};
let mut size: u32 = 0;
let err = unsafe { tester_raw::socketRecv(fd, &mut size) };
if err != 0 {
Err(anyhow!("Could not read response: {}", err))?;
}
Ok(HttpReply::unpacked(&get_result_bytes(size))?)
}
pub fn get(&self, account: AccountNumber, target: &str) -> Result<HttpReply, anyhow::Error> {
self.get_auth(account, target, "")
}
pub fn post(
&self,
account: AccountNumber,
target: &str,
data: HttpBody,
) -> Result<HttpReply, anyhow::Error> {
self.post_auth(account, target, data, "")
}
pub fn graphql<T: DeserializeOwned>(
&self,
account: AccountNumber,
query: &str,
) -> Result<T, anyhow::Error> {
self.graphql_auth(account, query, "")
}
pub fn get_auth(
&self,
account: AccountNumber,
target: &str,
token: &str,
) -> Result<HttpReply, anyhow::Error> {
let mut headers = Vec::new();
if !token.is_empty() {
headers.push(HttpHeader::new(
"Authorization",
&format!("Bearer {}", token),
));
}
self.http(&HttpRequest {
host: format!("{}.psibase.io", account),
method: "GET".into(),
target: target.into(),
contentType: "".into(),
body: <Vec<u8>>::new().into(),
headers,
})
}
pub fn post_auth(
&self,
account: AccountNumber,
target: &str,
data: HttpBody,
token: &str,
) -> Result<HttpReply, anyhow::Error> {
let mut headers = Vec::new();
if !token.is_empty() {
headers.push(HttpHeader::new(
"Authorization",
&format!("Bearer {}", token),
));
}
self.http(&HttpRequest {
host: format!("{}.psibase.io", account),
method: "POST".into(),
target: target.into(),
contentType: data.contentType,
body: data.body,
headers,
})
}
pub fn graphql_auth<T: DeserializeOwned>(
&self,
account: AccountNumber,
query: &str,
token: &str,
) -> Result<T, anyhow::Error> {
self.post_auth(account, "/graphql", HttpBody::graphql(query), token)?
.json()
}
pub fn login(
&self,
user: AccountNumber,
service: AccountNumber,
) -> Result<String, anyhow::Error> {
let expiration = TimePointSec::from(Utc::now()) + Seconds::new(10);
let tapos = Tapos {
expiration,
refBlockSuffix: 0,
flags: Tapos::DO_NOT_BROADCAST_FLAG,
refBlockIndex: 0,
};
let trx = Transaction {
tapos,
actions: vec![login_action(user, service, "psibase.io")],
claims: vec![],
};
let strx = SignedTransaction {
transaction: trx.packed().into(),
proofs: vec![],
};
let reply = self.post(
services::transact::SERVICE,
"/login",
HttpBody {
contentType: "application/octet-stream".into(),
body: strx.packed().into(),
},
)?;
#[derive(Deserialize)]
struct LoginReply {
access_token: String,
}
let login_reply: LoginReply = reply.json()?;
Ok(login_reply.access_token)
}
}
impl Chain {
pub fn fill_tapos(&self, trx: &mut Transaction, expire_seconds: u32) {
trx.tapos.expiration.seconds = expire_seconds as i64;
trx.tapos.refBlockIndex = 0;
trx.tapos.refBlockSuffix = 0;
if let Some(status) = &*self.status.borrow() {
trx.tapos.expiration =
status.current.time.seconds() + Seconds::new(expire_seconds as i64);
if let Some(head) = &status.head {
let mut suffix = [0; 4];
suffix.copy_from_slice(&head.blockId[head.blockId.len() - 4..]);
trx.tapos.refBlockIndex = (head.header.blockNum & 0x7f) as u8;
trx.tapos.refBlockSuffix = u32::from_le_bytes(suffix);
}
}
}
}
pub struct ChainEmptyResult {
pub trace: TransactionTrace,
}
impl ChainEmptyResult {
pub fn get(&self) -> Result<(), anyhow::Error> {
if let Some(e) = &self.trace.error {
Err(anyhow!("{}", e))
} else {
Ok(())
}
}
pub fn match_error(self, msg: &str) -> Result<TransactionTrace, anyhow::Error> {
self.trace.match_error(msg)
}
}
pub struct ChainResult<T: fracpack::UnpackOwned> {
pub trace: TransactionTrace,
_marker: PhantomData<T>,
}
impl<T: fracpack::UnpackOwned> ChainResult<T> {
pub fn get(&self) -> Result<T, anyhow::Error> {
self.get_with_debug(false)
}
fn is_user_action(act: &Action) -> bool {
use crate::{
self as psibase, method,
services::{cpu_limit, db, events, transact, virtual_server},
};
!(act.service == db::SERVICE && act.method == method!("open")
|| act.service == cpu_limit::SERVICE
|| act.sender == transact::SERVICE && act.service == virtual_server::SERVICE
|| act.service == events::SERVICE && act.method == method!("sync"))
}
pub fn get_with_debug(&self, debug: bool) -> Result<T, anyhow::Error> {
if let Some(e) = &self.trace.error {
return Err(anyhow!("{}", e));
}
if let Some(transact) = self.trace.action_traces.last() {
let ret = transact
.inner_traces
.iter()
.filter_map(|inner| {
if let InnerTraceEnum::ActionTrace(at) = &inner.inner {
if !Self::is_user_action(&at.action) {
return None;
}
if debug {
println!(
">>> ChainResult::get - Inner action trace: {} (raw_retval={})",
at.action.method, at.raw_retval
);
}
if at.raw_retval.is_empty() {
return None;
} else {
Some(&at.raw_retval)
}
} else {
None
}
})
.next();
if let Some(ret) = ret {
if debug {
println!(">>> ChainResult::get - Unpacking ret: `{}`", ret);
}
let unpacked_ret = T::unpacked(ret)?;
return Ok(unpacked_ret);
}
}
Err(anyhow!("Can't find action in trace"))
}
pub fn match_error(self, msg: &str) -> Result<TransactionTrace, anyhow::Error> {
self.trace.match_error(msg)
}
}
#[derive(Clone, Debug)]
pub struct ChainPusher<'a> {
pub chain: &'a Chain,
pub sender: AccountNumber,
pub service: AccountNumber,
}
impl<'a> Caller for ChainPusher<'a> {
type ReturnsNothing = ChainEmptyResult;
type ReturnType<T: fracpack::UnpackOwned> = ChainResult<T>;
fn call_returns_nothing<Args: fracpack::Pack>(
&self,
method: crate::MethodNumber,
args: Args,
) -> Self::ReturnsNothing {
let mut trx = Transaction {
tapos: Default::default(),
actions: vec![Action {
sender: self.sender,
service: self.service,
method,
rawData: args.packed().into(),
}],
claims: vec![],
};
self.chain.fill_tapos(&mut trx, 2);
let trace = self.chain.push(&SignedTransaction {
transaction: trx.packed().into(),
proofs: Default::default(),
});
if self.chain.is_auto_block_start {
self.chain.start_block();
}
ChainEmptyResult { trace }
}
fn call<Ret: fracpack::UnpackOwned, Args: fracpack::Pack>(
&self,
method: crate::MethodNumber,
args: Args,
) -> Self::ReturnType<Ret> {
let mut trx = Transaction {
tapos: Default::default(),
actions: vec![Action {
sender: self.sender,
service: self.service,
method,
rawData: args.packed().into(),
}],
claims: vec![],
};
self.chain.fill_tapos(&mut trx, 2);
let trace = self.chain.push(&SignedTransaction {
transaction: trx.packed().into(),
proofs: Default::default(),
});
let ret = ChainResult::<Ret> {
trace,
_marker: Default::default(),
};
if self.chain.is_auto_block_start {
self.chain.start_block();
}
ret
}
}