Skip to content

Commit

Permalink
Adapt to rs-stellar-xdr Limits change (#1139)
Browse files Browse the repository at this point in the history
### What

- Adapts to stellar/rs-stellar-xdr#317 and runs
perf analysis.
- Make `DepthLimiter` internal (the depth limiting mechanism for the
host stays the same)
 - Defines the default XDR read/write limits (depth, length). 

### Why

[TODO: Why this change is being made. Include any context required to
understand the why.]

### Known limitations

[TODO or N/A]

---------

Co-authored-by: Graydon Hoare <[email protected]>
  • Loading branch information
jayz22 and graydon authored Nov 10, 2023
1 parent ac39ea2 commit dfceb3d
Show file tree
Hide file tree
Showing 29 changed files with 292 additions and 155 deletions.
9 changes: 6 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 1 addition & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ soroban-builtin-sdk-macros = { version = "20.0.0-rc2", path = "soroban-builtin-s
[workspace.dependencies.stellar-xdr]
version = "20.0.0-rc1"
git = "https://github.com/stellar/rs-stellar-xdr"
rev = "9c97e4fa909a0b6455547a4f4a95800696b2a69a"
rev = "957273d7b8092888849219a3ad2aa054059ca89d"
default-features = false

[workspace.dependencies.wasmi]
Expand All @@ -37,11 +37,6 @@ version = "0.31.0-soroban1"
git = "https://github.com/stellar/wasmi"
rev = "7e63b4c9e08c4163f417d118d81f7ea34789d0be"

[workspace.dependencies.stellar-strkey]
version = "0.0.7"
git = "https://github.com/stellar/rs-stellar-strkey"
rev = "e6ba45c60c16de28c7522586b80ed0150157df73"

# [patch."https://github.com/stellar/rs-stellar-xdr"]
# stellar-xdr = { path = "../rs-stellar-xdr/" }
# [patch."https://github.com/stellar/wasmi"]
Expand Down
2 changes: 1 addition & 1 deletion soroban-env-common/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ impl From<ConversionError> for Error {
impl From<crate::xdr::Error> for Error {
fn from(e: crate::xdr::Error) -> Self {
match e {
crate::xdr::Error::DepthLimitExceeded => {
crate::xdr::Error::DepthLimitExceeded | crate::xdr::Error::LengthLimitExceeded => {
Error::from_type_and_code(ScErrorType::Context, ScErrorCode::ExceededLimit)
}
_ => Error::from_type_and_code(ScErrorType::Value, ScErrorCode::InvalidInput),
Expand Down
2 changes: 1 addition & 1 deletion soroban-env-host/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ build = "build.rs"
[dependencies]
soroban-builtin-sdk-macros = { workspace = true }
soroban-env-common = { workspace = true, features = ["std", "wasmi"] }
stellar-strkey = { workspace = true }
wasmi = { workspace = true }
stellar-strkey = "0.0.8"
static_assertions = "1.1.0"
sha2 = "0.10.0"
ed25519-dalek = {version = "2.0.0", features = ["rand_core"] }
Expand Down
10 changes: 2 additions & 8 deletions soroban-env-host/benches/common/cost_types/host_mem_cmp.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
use crate::HostCostMeasurement;
use rand::{rngs::StdRng, RngCore};
use crate::{common::util::randvec, HostCostMeasurement};
use rand::rngs::StdRng;
use soroban_env_host::{cost_runner::*, Host};

fn randvec(rng: &mut StdRng, len: u64) -> Vec<u8> {
let mut res: Vec<u8> = vec![0; len as usize];
rng.fill_bytes(res.as_mut_slice());
res
}

pub struct MemCmpMeasure;
impl HostCostMeasurement for MemCmpMeasure {
type Runner = MemCmpRun;
Expand Down
8 changes: 5 additions & 3 deletions soroban-env-host/benches/common/cost_types/val_deser.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use soroban_env_host::{cost_runner::ValDeserRun, xdr::ScVal, xdr::WriteXdr};
use soroban_env_host::{
cost_runner::ValDeserRun, xdr::ScVal, xdr::WriteXdr, DEFAULT_XDR_RW_LIMITS,
};

use super::{ValSerMeasure, MAX_DEPTH};
use crate::common::HostCostMeasurement;
Expand All @@ -14,7 +16,7 @@ impl HostCostMeasurement for ValDeserMeasure {
input: u64,
) -> Vec<u8> {
let scval = ValSerMeasure::new_random_case(host, rng, input);
scval.0.to_xdr().unwrap()
scval.0.to_xdr(DEFAULT_XDR_RW_LIMITS).unwrap()
}

// The worst case is a deeply nested structure, each level containing minimal
Expand All @@ -37,6 +39,6 @@ impl HostCostMeasurement for ValDeserMeasure {
v = ScVal::Vec(Some(inner.try_into().unwrap()));
rem -= elem_per_level;
}
v.to_xdr().unwrap()
v.to_xdr(DEFAULT_XDR_RW_LIMITS).unwrap()
}
}
1 change: 1 addition & 0 deletions soroban-env-host/benches/common/cost_types/val_ser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub(crate) struct ValSerMeasure;
// This measures the costs of converting an ScVal into bytes.
impl HostCostMeasurement for ValSerMeasure {
type Runner = ValSerRun;
const STEP_SIZE: u64 = 512;

fn new_random_case(_host: &Host, rng: &mut StdRng, input: u64) -> (ScVal, Vec<u8>) {
let len = 1 + input * Self::STEP_SIZE;
Expand Down
2 changes: 2 additions & 0 deletions soroban-env-host/benches/common/experimental/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod ed25519_scalar_mul;
mod read_xdr;

pub(crate) use ed25519_scalar_mul::*;
pub(crate) use read_xdr::*;
24 changes: 24 additions & 0 deletions soroban-env-host/benches/common/experimental/read_xdr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use soroban_env_host::{
cost_runner::ReadXdrByteArrayRun,
xdr::{ScBytes, ScVal, WriteXdr},
DEFAULT_XDR_RW_LIMITS,
};

use crate::common::{util::randvec, HostCostMeasurement};

pub(crate) struct ReadXdrByteArrayMeasure;

impl HostCostMeasurement for ReadXdrByteArrayMeasure {
type Runner = ReadXdrByteArrayRun;

fn new_random_case(
_host: &soroban_env_host::Host,
rng: &mut rand::prelude::StdRng,
input: u64,
) -> Vec<u8> {
let len = 1 + input * Self::STEP_SIZE;
let vec = randvec(rng, len);
let scval = ScVal::Bytes(ScBytes(vec.try_into().unwrap()));
scval.to_xdr(DEFAULT_XDR_RW_LIMITS).unwrap()
}
}
9 changes: 6 additions & 3 deletions soroban-env-host/benches/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,12 @@ fn call_bench<B: Benchmark, HCM: HostCostMeasurement>(
}

pub(crate) fn for_each_experimental_cost_measurement<B: Benchmark>(
) -> std::io::Result<(FPCostModel, FPCostModel)> {
B::bench::<Ed25519ScalarMulMeasure>()?;
B::bench::<VerifyEd25519SigMeasure>()
) -> std::io::Result<BTreeMap<CostType, (FPCostModel, FPCostModel)>> {
let mut params: BTreeMap<CostType, (FPCostModel, FPCostModel)> = BTreeMap::new();
call_bench::<B, Ed25519ScalarMulMeasure>(&mut params)?;
call_bench::<B, VerifyEd25519SigMeasure>(&mut params)?;
call_bench::<B, ReadXdrByteArrayMeasure>(&mut params)?;
Ok(params)
}

pub(crate) fn for_each_host_cost_measurement<B: Benchmark>(
Expand Down
7 changes: 7 additions & 0 deletions soroban-env-host/benches/common/util.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use rand::{rngs::StdRng, RngCore};
use soroban_env_host::{budget::AsBudget, meta, Host, LedgerInfo, Val, I256};

pub(crate) fn test_host() -> Host {
Expand Down Expand Up @@ -35,3 +36,9 @@ pub(crate) fn repeating_byte_i256(byte: u8, input: u64) -> I256 {
res.extend_from_slice(buf.as_slice());
I256::from_be_bytes(res.try_into().unwrap())
}

pub(crate) fn randvec(rng: &mut StdRng, len: u64) -> Vec<u8> {
let mut res: Vec<u8> = vec![0; len as usize];
rng.fill_bytes(res.as_mut_slice());
res
}
7 changes: 6 additions & 1 deletion soroban-env-host/benches/worst_case_linear_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// $ cargo bench --features testutils --bench worst_case_linear_models -- --nocapture
// You can optionally pass in args listing the {`ContractCostType`, `WasmInsnType`} combination to run with, e.g.
// $ cargo bench --features testutils --bench worst_case_linear_models -- MemCpy I64Rotr --nocapture
// To run the experimental cost types: $ RUN_EXPERIMENT=1 cargo bench ...
mod common;
use common::*;
use soroban_env_host::{
Expand Down Expand Up @@ -176,7 +177,11 @@ fn extract_wasmi_fuel_costs(

#[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
fn main() -> std::io::Result<()> {
let params = for_each_host_cost_measurement::<WorstCaseLinearModels>()?;
let params = if std::env::var("RUN_EXPERIMENT").is_err() {
for_each_host_cost_measurement::<WorstCaseLinearModels>()?
} else {
for_each_experimental_cost_measurement::<WorstCaseLinearModels>()?
};
let params_wasm = for_each_wasm_insn_measurement::<WorstCaseLinearModels>()?;

let mut tw = TabWriter::new(vec![])
Expand Down
52 changes: 7 additions & 45 deletions soroban-env-host/src/budget.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
mod dimension;
mod limits;
mod model;
mod util;
mod wasmi_helper;

pub(crate) use limits::DepthLimiter;
pub use limits::{DEFAULT_HOST_DEPTH_LIMIT, DEFAULT_XDR_RW_LIMITS};
pub use model::COST_MODEL_LIN_TERM_SCALE_BITS;

use std::{
Expand All @@ -13,19 +16,14 @@ use std::{

use crate::{
host::error::TryBorrowOrErr,
xdr::{ContractCostParams, ContractCostType, DepthLimiter, ScErrorCode, ScErrorType},
Error, Host, HostError, DEFAULT_HOST_DEPTH_LIMIT,
xdr::{ContractCostParams, ContractCostType, ScErrorCode, ScErrorType},
Host, HostError,
};

use dimension::{BudgetDimension, IsCpu, IsShadowMode};
use model::ScaledU64;
use wasmi_helper::FuelConfig;

// These are some sane values, however the embedder should typically customize
// these to match the network config.
const DEFAULT_CPU_INSN_LIMIT: u64 = 100_000_000;
const DEFAULT_MEM_BYTES_LIMIT: u64 = 40 * 1024 * 1024; // 40MB

#[derive(Clone, Default)]
struct MeterTracker {
// Tracks the `(sum_of_iterations, total_input)` for each `CostType`
Expand Down Expand Up @@ -423,8 +421,8 @@ impl Default for BudgetImpl {
}

// define the limits
b.cpu_insns.reset(DEFAULT_CPU_INSN_LIMIT);
b.mem_bytes.reset(DEFAULT_MEM_BYTES_LIMIT);
b.cpu_insns.reset(limits::DEFAULT_CPU_INSN_LIMIT);
b.mem_bytes.reset(limits::DEFAULT_MEM_BYTES_LIMIT);
b
}
}
Expand Down Expand Up @@ -527,30 +525,6 @@ impl Display for BudgetImpl {
}
}

impl DepthLimiter for BudgetImpl {
type DepthLimiterError = HostError;

fn enter(&mut self) -> Result<(), HostError> {
if let Some(depth) = self.depth_limit.checked_sub(1) {
self.depth_limit = depth;
} else {
return Err(Error::from_type_and_code(
ScErrorType::Context,
ScErrorCode::ExceededLimit,
)
.into());
}
Ok(())
}

// `leave` should be called in tandem with `enter` such that the depth
// doesn't exceed the initial depth limit.
fn leave(&mut self) -> Result<(), HostError> {
self.depth_limit = self.depth_limit.saturating_add(1);
Ok(())
}
}

#[derive(Clone)]
pub struct Budget(pub(crate) Rc<RefCell<BudgetImpl>>);

Expand Down Expand Up @@ -603,18 +577,6 @@ impl AsBudget for &Host {
}
}

impl DepthLimiter for Budget {
type DepthLimiterError = HostError;

fn enter(&mut self) -> Result<(), HostError> {
self.0.try_borrow_mut_or_err()?.enter()
}

fn leave(&mut self) -> Result<(), HostError> {
self.0.try_borrow_mut_or_err()?.leave()
}
}

impl Budget {
/// Initializes the budget from network configuration settings.
pub fn try_from_configs(
Expand Down
99 changes: 99 additions & 0 deletions soroban-env-host/src/budget/limits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use crate::{
budget::{Budget, BudgetImpl},
host::error::TryBorrowOrErr,
xdr::{Limits, ScErrorCode, ScErrorType},
Error, HostError,
};

/// These constants are used to set limits on recursion and data length in the
/// context of XDR (de)serialization. They serve as safeguards agaist both
/// exccessive stack allocation, which could cause an unrecoverable `SIGABRT`,
/// and exccessive heap memory allocation.
pub const DEFAULT_XDR_RW_LIMITS: Limits = Limits {
/// recursion limit for reading and writing XDR structures.
depth: 500,
/// maximum byte length for a data structure during serialization and
/// deserialization to and from the XDR format. The limit of 16MiB
/// corresponds to the overlay maximum message size.
len: 0x1000000,
};

/// - `DEFAULT_HOST_DEPTH_LIMIT`: This limit applies to the host environment. It
/// guards recursion paths involving the `Env` and `Budget`, particularly
/// during operations like conversion, comparison, and deep cloning. The limit
/// is strategically checked at critical recursion points, such as when
/// encountering a `Val`. As the actual stack usage can be higher,
/// `DEFAULT_HOST_DEPTH_LIMIT` is conservatively set to a lower value than the
/// XDR limit.
pub const DEFAULT_HOST_DEPTH_LIMIT: u32 = 100;

// These are some sane values, however the embedder should typically customize
// these to match the network config.
pub(crate) const DEFAULT_CPU_INSN_LIMIT: u64 = 100_000_000;
pub(crate) const DEFAULT_MEM_BYTES_LIMIT: u64 = 40 * 1024 * 1024; // 40MB

/// `DepthLimiter` is a trait designed for managing the depth of recursive operations.
/// It provides a mechanism to limit recursion depth, and defines the behavior upon
/// entering and leaving a recursion level.
pub(crate) trait DepthLimiter {
/// Defines the behavior for entering a new recursion level.
/// An `ExceededLimit` is returned if the new level exceeds the depth limit.
fn enter(&mut self) -> Result<(), HostError>;

/// Defines the behavior for leaving a recursion level.
fn leave(&mut self) -> Result<(), HostError>;

/// Wraps a given function `f` with depth limiting guards.
/// It triggers an `enter` before, and a `leave` after the execution of `f`.
///
/// # Parameters
///
/// - `f`: The function to be executed under depth limit constraints.
///
/// # Returns
///
/// - `Err` if 1. the depth limit has been exceeded upon `enter` 2. `f` executes
/// with an error 3. if error occurs on `leave`.
/// `Ok` otherwise.
fn with_limited_depth<T, F>(&mut self, f: F) -> Result<T, HostError>
where
F: FnOnce(&mut Self) -> Result<T, HostError>,
{
self.enter()?;
let res = f(self);
self.leave()?;
res
}
}

impl DepthLimiter for BudgetImpl {
fn enter(&mut self) -> Result<(), HostError> {
if let Some(depth) = self.depth_limit.checked_sub(1) {
self.depth_limit = depth;
} else {
return Err(Error::from_type_and_code(
ScErrorType::Context,
ScErrorCode::ExceededLimit,
)
.into());
}
Ok(())
}

// `leave` should be called in tandem with `enter` such that the depth
// doesn't exceed the initial depth limit.
fn leave(&mut self) -> Result<(), HostError> {
self.depth_limit = self.depth_limit.saturating_add(1);
Ok(())
}
}

impl DepthLimiter for Budget {
fn enter(&mut self) -> Result<(), HostError> {
self.0.try_borrow_mut_or_err()?.enter()
}

fn leave(&mut self) -> Result<(), HostError> {
self.0.try_borrow_mut_or_err()?.leave()
}
}
Loading

0 comments on commit dfceb3d

Please sign in to comment.