use std::io::Write;
use futures::future::BoxFuture;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use sha2::{Digest, Sha256};
use wasmtime::Caller;
use super::StoreData;
use super::durable::EffectId;
use super::fuel;
use super::host::{self, fatal_host_error};
pub const MAX_OUTCOME_BYTES: usize = 64 * 1024 * 1024;
pub const CODEC_VERSION: u32 = 4;
#[derive(Debug, thiserror::Error)]
#[error("durable execution: {0}")]
pub struct HistoryError(pub String);
pub struct EffectCall {
pub id: EffectId,
pub sequence: u64,
pub recorded: Option<RecordedOutcome>,
}
pub type RecordedOutcome = Result<OutcomeBytes, NativeError>;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct OutcomeBytes(#[serde(with = "binary")] pub Vec<u8>);
#[async_trait::async_trait]
pub trait EffectHistory: Send + Sync {
async fn begin(
&self,
operation: &str,
fingerprint: [u8; 32],
) -> Result<EffectCall, HistoryError>;
async fn complete(
&self,
call: &EffectCall,
outcome: RecordedOutcome,
) -> Result<(), HistoryError>;
async fn ensure_history_consumed(&self) -> Result<(), HistoryError>;
}
pub async fn execute<'store, A, T, F>(
caller: &mut Caller<'store, StoreData>,
operation: &'static str,
arguments: A,
action: F,
) -> wasmtime::Result<T>
where
A: Serialize + Send + 'static,
T: Serialize + DeserializeOwned + Send + 'static,
F: for<'call> FnOnce(
&'call mut Caller<'store, StoreData>,
EffectId,
A,
) -> BoxFuture<'call, wasmtime::Result<T>>
+ Send,
{
let Some(history) = caller.data().effects.clone() else {
let result = action(caller, EffectId::stub()?, arguments).await;
return result.map_err(|error| host::throw_host_error(caller, error));
};
let fingerprint = fingerprint(caller, &arguments)?;
let call = history
.begin(operation, fingerprint)
.await
.map_err(fatal_host_error)?;
if let Some(recorded) = &call.recorded {
return match recorded {
Ok(bytes) => {
fuel::charge(&mut *caller, fuel::PARSE, bytes.0.len() as u64)?;
decode(&bytes.0)
}
Err(error) => Err(host::throw_host_error(caller, error.clone().into_error())),
};
}
let result = action(caller, call.id.clone(), arguments).await;
if let Err(error) = &result
&& (host::ends_the_run(error) || error.is::<wasmtime::ThrownException>())
{
return result;
}
let recorded_result = match &result {
Ok(value) => {
let encoded = encode(value)?;
validate_outcome(&encoded)?;
fuel::settle(&mut *caller, fuel::COPY, encoded.len() as u64)?;
Ok(OutcomeBytes(encoded))
}
Err(error) => Err(NativeError::capture(error)),
};
history
.complete(&call, recorded_result)
.await
.map_err(fatal_host_error)?;
result.map_err(|error| host::throw_host_error(caller, error))
}
pub(crate) mod binary {
use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_bytes(bytes)
}
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
struct Bytes;
impl<'de> serde::de::Visitor<'de> for Bytes {
type Value = Vec<u8>;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("binary outcome")
}
fn visit_bytes<E: serde::de::Error>(self, bytes: &[u8]) -> Result<Vec<u8>, E> {
Ok(bytes.to_vec())
}
}
deserializer.deserialize_bytes(Bytes)
}
}
fn fingerprint<A: Serialize>(
caller: &mut Caller<'_, StoreData>,
arguments: &A,
) -> wasmtime::Result<[u8; 32]> {
let mut writer = FingerprintWriter {
caller,
hash: Sha256::new(),
failure: None,
};
let result =
arguments.serialize(&mut rmp_serde::Serializer::new(&mut writer).with_struct_map());
if let Some(error) = writer.failure {
return Err(error);
}
result.map_err(fatal_host_error)?;
Ok(writer.hash.finalize().into())
}
struct FingerprintWriter<'a, 'store> {
caller: &'a mut Caller<'store, StoreData>,
hash: Sha256,
failure: Option<wasmtime::Error>,
}
impl Write for FingerprintWriter<'_, '_> {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
if let Err(error) = fuel::charge(&mut *self.caller, fuel::HASH, bytes.len() as u64) {
self.failure = Some(error);
return Err(std::io::Error::other("effect fingerprint exceeded fuel"));
}
self.hash.update(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn encode<T: Serialize>(value: &T) -> wasmtime::Result<Vec<u8>> {
let mut writer = OutcomeWriter { bytes: Vec::new() };
value
.serialize(&mut rmp_serde::Serializer::new(&mut writer).with_struct_map())
.map_err(fatal_host_error)?;
Ok(writer.bytes)
}
struct OutcomeWriter {
bytes: Vec<u8>,
}
impl Write for OutcomeWriter {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
let length = self
.bytes
.len()
.checked_add(bytes.len())
.filter(|n| *n <= MAX_OUTCOME_BYTES)
.ok_or_else(|| std::io::Error::other("native outcome exceeds history limit"))?;
if length > self.bytes.capacity() {
let capacity = length
.max(self.bytes.capacity().saturating_mul(2))
.min(MAX_OUTCOME_BYTES);
self.bytes
.try_reserve_exact(capacity - self.bytes.len())
.map_err(std::io::Error::other)?;
}
self.bytes.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn validate_outcome(bytes: &[u8]) -> wasmtime::Result<()> {
if bytes.len() > MAX_OUTCOME_BYTES {
return Err(fatal_host_error("native outcome exceeds history limit"));
}
super::effects_memory::decoded_heap_bound(bytes, 1, false)?;
Ok(())
}
fn decode<T: DeserializeOwned>(bytes: &[u8]) -> wasmtime::Result<T> {
validate_outcome(bytes)?;
let mut decoder = rmp_serde::Deserializer::new(std::io::Cursor::new(bytes));
decoder.set_max_depth(128);
let value = T::deserialize(&mut decoder).map_err(fatal_host_error)?;
if decoder.position() != bytes.len() as u64 {
return Err(fatal_host_error("trailing native outcome bytes"));
}
Ok(value)
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum NativeError {
Error(String),
Range(String),
QuotaExceeded(String),
Timeout(String),
Type(String),
Syntax(String),
Uri(String),
PermissionDenied(host::PermissionDenied),
}
impl NativeError {
pub(crate) fn capture(error: &wasmtime::Error) -> Self {
let message = error.to_string();
if let Some(denial) = error.downcast_ref::<host::PermissionDenied>() {
return Self::PermissionDenied(denial.clone());
}
if error.is::<host::RangeError>() {
return Self::Range(message);
}
if error.is::<host::QuotaExceededError>() {
return Self::QuotaExceeded(message);
}
if error.is::<host::TimeoutError>() {
return Self::Timeout(message);
}
if error.is::<host::TypeError>() {
return Self::Type(message);
}
if error.is::<host::SyntaxError>() {
return Self::Syntax(message);
}
if error.is::<host::UriError>() {
return Self::Uri(message);
}
Self::Error(message)
}
pub(crate) fn into_error(self) -> wasmtime::Error {
match self {
Self::Error(message) => wasmtime::Error::msg(message),
Self::Range(message) => wasmtime::Error::new(host::RangeError(message)),
Self::QuotaExceeded(message) => wasmtime::Error::new(host::QuotaExceededError(message)),
Self::Timeout(message) => wasmtime::Error::new(host::TimeoutError(message)),
Self::Type(message) => wasmtime::Error::new(host::TypeError(message)),
Self::Syntax(message) => wasmtime::Error::new(host::SyntaxError(message)),
Self::Uri(message) => wasmtime::Error::new(host::UriError(message)),
Self::PermissionDenied(denial) => wasmtime::Error::new(denial),
}
}
}
pub fn stored_heap_bound(
bytes: &[u8],
element_bytes: usize,
dynamic_maps: bool,
) -> Result<u64, HistoryError> {
super::effects_memory::decoded_heap_bound(bytes, element_bytes, dynamic_maps)
.map_err(|error| HistoryError(error.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn native_floats_keep_nan_bits_and_signed_zero() {
for bits in [
0x8000_0000_0000_0000,
0x7ff8_0000_0000_1234,
0x7ff0_0000_0000_0000,
] {
let encoded = encode(&f64::from_bits(bits)).unwrap();
let restored: f64 = rmp_serde::from_slice(&encoded).unwrap();
assert_eq!(restored.to_bits(), bits);
}
}
#[test]
fn binary_is_not_expanded_into_integer_elements() {
let value = OutcomeBytes(vec![255; 4096]);
let encoded = encode(&value).unwrap();
assert!(encoded.len() < 4100);
let restored: OutcomeBytes = rmp_serde::from_slice(&encoded).unwrap();
assert_eq!(restored, value);
drop(encoded);
}
#[test]
fn missing_and_empty_native_strings_remain_distinct() {
for value in [None, Some(String::new()), Some("text".to_owned())] {
let encoded = encode(&value).unwrap();
let restored: Option<String> = rmp_serde::from_slice(&encoded).unwrap();
assert_eq!(restored, value);
}
}
}