thruput-json-rpc-server 0.43.0

Reusable JSON-RPC server layer for Thruput
Documentation
use super::invalid;
use crate::{RpcServerError, TipReading};
use std::str::FromStr;
use thruput_json_rpc_types::{network_config as legacy, responses as out};

pub(super) fn tip(
    value: &thruput_json_rpc_types::block::ChainTip,
) -> Result<out::ChainTip, RpcServerError> {
    super::service::snapshot(&value.height)?;
    Ok(out::ChainTip {
        block_height: value.height.clone(),
        block_hash: value.block_hash,
    })
}

fn amount<T: FromStr + ToString>(value: &str) -> Result<T, RpcServerError> {
    let parsed: T = value
        .parse()
        .map_err(|_| invalid("invalid storage price"))?;
    if parsed.to_string() != value {
        return Err(invalid("noncanonical storage price"));
    }
    Ok(parsed)
}

fn limits(value: legacy::NetworkConfigParams) -> out::ConfigLimits {
    let legacy::NetworkConfigParams {
        max_sha256_buffers,
        max_cross_invoke_depth,
        max_new_contracts_per_tx,
        max_bytestring_len,
        max_contract_size_bytes,
        max_savepoint_depth,
        max_auth_token_depth,
        max_rv32_code_bytes,
        max_rv32_memory_bytes,
        max_tx_gas,
    } = value;
    out::ConfigLimits {
        max_sha256_buffers,
        max_cross_invoke_depth,
        max_new_contracts_per_tx,
        max_bytestring_len,
        max_contract_size_bytes,
        max_savepoint_depth,
        max_auth_token_depth,
        max_rv32_code_bytes,
        max_rv32_memory_bytes,
        max_tx_gas,
    }
}

pub(super) fn config(reading: TipReading) -> Result<out::ChainConfig, RpcServerError> {
    tip(&reading.tip)?;
    let stated = legacy::NetworkConfigParams::from_network_config(&reading.network_config)
        .map_err(|error| {
            RpcServerError::invalid_data(
                thruput_json_rpc_types::error::native::BackendDataInvalidReason::UnsupportedValue,
                error.to_string(),
            )
        })?;
    let mut result = out::ChainConfig::new(limits(stated));
    if let Some(execution) = reading.execution {
        if execution.observed_tip.height != reading.tip.height
            || execution.observed_tip.block_hash != reading.tip.block_hash
        {
            return Err(invalid(
                "execution configuration is bound to a different tip",
            ));
        }
        let prices = &execution.storage_costs;
        result.execution = Some(out::ExecutionConfig {
            observed_tip: tip(&execution.observed_tip)?,
            config_hash: execution.config_hash,
            effective_limits: limits(execution.effective_limits),
            storage_costs: out::StorageCosts {
                base_data_item_cost: amount(&prices.base_data_item_cost)?,
                bytestring_cost_per_byte: amount(&prices.bytestring_cost_per_byte)?,
                base_contract_deployment_cost: amount(&prices.base_contract_deployment_cost)?,
                contract_deployment_cost_per_byte: amount(
                    &prices.contract_deployment_cost_per_byte,
                )?,
                auth_per_key_cost: amount(&prices.auth_per_key_cost)?,
                rcq_cost_per_entry: amount(&prices.rcq_cost_per_entry)?,
                replay_cache_cost_per_entry: amount(&prices.replay_cache_cost_per_entry)?,
                balance_cost: amount(&prices.balance_cost)?,
            },
        });
    }
    Ok(result)
}

#[cfg(test)]
mod tests {
    use super::*;
    use thruput_json_rpc_types::block::ChainTip;
    use thruput_xdr::network_config::{NetworkConfig, NetworkConfigV0};
    use xdr_common::VecM;

    fn reading() -> TipReading {
        TipReading::new(
            ChainTip {
                height: 7.into(),
                block_hash: thruput_json_rpc_types::base58::Base58(
                    thruput_xdr::types::SHA256Hash::from([8; 32]),
                ),
            },
            NetworkConfig::_0 {
                v0: NetworkConfigV0 {
                    params: VecM(vec![]),
                },
            },
        )
    }
    fn execution() -> legacy::ExecutionConfig {
        let prices = serde_json::from_value(serde_json::json!({
            "baseDataItemCost":"9007199254740993", "bytestringCostPerByte":"2", "baseContractDeploymentCost":"3", "contractDeploymentCostPerByte":"4", "authPerKeyCost":"5", "rcqCostPerEntry":"6", "replayCacheCostPerEntry":"7", "balanceCost":"8"
        })).unwrap();
        legacy::ExecutionConfig::new(
            reading().tip,
            thruput_json_rpc_types::base58::Base58(thruput_xdr::types::SHA256Hash::from([9; 32])),
            legacy::NetworkConfigParams::default(),
            prices,
        )
    }
    #[test]
    fn missing_execution_and_limits_are_not_fabricated() {
        let value = serde_json::to_value(config(reading()).unwrap()).unwrap();
        assert!(value.get("execution").is_none());
        assert!(value["stated"].as_object().unwrap().is_empty());
    }
    #[test]
    fn execution_provenance_and_prices_are_preserved_or_rejected() {
        let value =
            serde_json::to_value(config(reading().with_execution(execution())).unwrap()).unwrap();
        assert_eq!(value["execution"]["observedTip"]["blockHeight"], "7");
        assert_eq!(
            value["execution"]["storageCosts"]["baseDataItemCost"],
            "9007199254740993"
        );
        for malformed in ["-1", "01", "18446744073709551616"] {
            let mut execution = execution();
            execution.storage_costs.balance_cost = malformed.into();
            assert!(matches!(
                config(reading().with_execution(execution)),
                Err(RpcServerError::BackendDataInvalid { .. })
            ));
        }
        let mut execution = execution();
        execution.observed_tip.height = 8.into();
        assert!(config(reading().with_execution(execution)).is_err());
    }
}