Skip to main content

thruput_tx_validation/
lib.rs

1//! Off-chain mirror of the bounds hogd checks before executing a
2//! transaction.
3//!
4//! hogd's `SystemCallHandler::perform_initial_validation` rejects a
5//! transaction deterministically, before running any of it, when its root
6//! invocation budgets more gas than the chain's `max_tx_gas`, when it
7//! attaches more contracts than the chain's network config permits, when
8//! any attached contract's code exceeds the size the config admits, when
9//! the same contract is attached twice, or when an attached RV32 artifact
10//! declares more guest memory or code than the config admits (the
11//! attachment fails verification in `NewContractSet::make`). Applying the
12//! same checks before submission turns a silent later failure into an
13//! immediate, structured [`TxInvalidDetail`].
14//!
15//! The chain-configured bounds are read from the network config a db tip
16//! carries, flattened once into [`NetworkConfigParams`] so every later
17//! access is a field read. A bound is enforced only with a value the
18//! protocol defines: the one the config states, or, for the parameters
19//! whose unstated value the protocol itself fixes
20//! (`kDefaultMaxContractSizeBytes`, `kDefaultMaxRv32CodeBytes`,
21//! `kDefaultMaxRv32MemoryBytes`, and `max_tx_gas`'s 1,000,000,000), that
22//! constant. Nothing is copied from
23//! hogd's own fallbacks. None of this is a trust boundary: the executor
24//! re-checks everything, and a reading that lags a block is harmless
25//! because these parameters change only through explicit config upgrades.
26
27use thruput_json_rpc_types::error::TxInvalidDetail;
28use thruput_rv32_artifact::{
29    DEFAULT_MAX_CODE_BYTES, DEFAULT_MEMORY_BYTES, DeclaredSizes, declared_sizes,
30};
31use thruput_xdr::{
32    contract::{Contract, kDefaultMaxContractSizeBytes},
33    transaction::Transaction,
34};
35
36/// `kDefaultMaxRv32CodeBytes` from the protocol's `hog_exec_constants.x`: the
37/// code bound in force for a configuration that does not state one.
38///
39/// The Rust protocol bindings do not bind that file yet, so the value is
40/// pinned here and held to the artifact crate's restatement of it.
41const DEFAULT_MAX_RV32_CODE_BYTES: u32 = 262_144;
42/// `kDefaultMaxRv32MemoryBytes` from `hog_exec_constants.x`: the guest memory
43/// bound in force for a configuration that does not state one.
44const DEFAULT_MAX_RV32_MEMORY_BYTES: u32 = 65_536;
45/// The `max_tx_gas` in force for a configuration that does not state one, as
46/// the protocol documents it on `kConfigParamMaxTxGas`. The bindings carry no
47/// constant for it, so it is pinned here.
48const DEFAULT_MAX_TX_GAS: u64 = 1_000_000_000;
49const _: () = assert!(DEFAULT_MAX_RV32_CODE_BYTES as usize == DEFAULT_MAX_CODE_BYTES);
50const _: () = assert!(DEFAULT_MAX_RV32_MEMORY_BYTES as usize == DEFAULT_MEMORY_BYTES);
51
52pub use thruput_json_rpc_types::network_config::{NetworkConfigParams, UnsupportedNetworkConfig};
53
54/// Bytes one attached contract's code blob may hold under `params`.
55///
56/// The protocol fixes `kDefaultMaxContractSizeBytes` as the value in
57/// force for a configuration that does not state the parameter, since
58/// every chain predating it ran under that cap. Unlike the other bounds,
59/// this one is therefore always known.
60fn max_contract_size_bytes_in_force(params: &NetworkConfigParams) -> u32 {
61    params
62        .max_contract_size_bytes
63        .unwrap_or(kDefaultMaxContractSizeBytes)
64}
65
66/// Code bytes an attached RV32 artifact may carry under `params`.
67///
68/// The protocol fixes `kDefaultMaxRv32CodeBytes` as the value in force
69/// for a configuration that does not state the parameter, so, like the
70/// contract size, this bound is always known.
71fn max_rv32_code_bytes_in_force(params: &NetworkConfigParams) -> u32 {
72    params
73        .max_rv32_code_bytes
74        .unwrap_or(DEFAULT_MAX_RV32_CODE_BYTES)
75}
76
77/// Gas a transaction's root invocation may budget under `params`; the
78/// protocol's 1,000,000,000 when unstated, so this bound is always known.
79fn max_tx_gas_in_force(params: &NetworkConfigParams) -> u64 {
80    params.max_tx_gas.unwrap_or(DEFAULT_MAX_TX_GAS)
81}
82
83/// Guest data memory bytes an attached RV32 artifact may declare under
84/// `params`; `kDefaultMaxRv32MemoryBytes` when unstated.
85fn max_rv32_memory_bytes_in_force(params: &NetworkConfigParams) -> u32 {
86    params
87        .max_rv32_memory_bytes
88        .unwrap_or(DEFAULT_MAX_RV32_MEMORY_BYTES)
89}
90
91/// The name a chain execution parameter carries outside its XDR encoding.
92///
93/// The protocol encodes each parameter as a `ConfigParam` union arm; every
94/// surface that has to *say* which parameter it means spells the arm's field
95/// name instead. hog-devnode's `--network-config NAME=VALUE` flag keys its
96/// genesis overrides by these names and records them under the same names in
97/// a data directory's bootstrap metadata. This enum is the one place the
98/// Rust side spells them, so a parameter cannot reach hog-devnode under a
99/// name no parameter has.
100///
101/// Naming a parameter says nothing about which values it admits; that stays
102/// hog-devnode's call, against the ceilings its build was compiled with.
103#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
104#[non_exhaustive]
105pub enum ConfigParamName {
106    /// [`NetworkConfigParams::max_sha256_buffers`].
107    MaxSha256Buffers,
108    /// [`NetworkConfigParams::max_cross_invoke_depth`].
109    MaxCrossInvokeDepth,
110    /// [`NetworkConfigParams::max_new_contracts_per_tx`].
111    MaxNewContractsPerTx,
112    /// [`NetworkConfigParams::max_bytestring_len`].
113    MaxBytestringLen,
114    /// [`NetworkConfigParams::max_contract_size_bytes`].
115    MaxContractSizeBytes,
116    /// [`NetworkConfigParams::max_savepoint_depth`].
117    MaxSavepointDepth,
118    /// [`NetworkConfigParams::max_auth_token_depth`].
119    MaxAuthTokenDepth,
120    /// [`NetworkConfigParams::max_rv32_code_bytes`].
121    MaxRv32CodeBytes,
122    /// [`NetworkConfigParams::max_rv32_memory_bytes`].
123    MaxRv32MemoryBytes,
124    /// [`NetworkConfigParams::max_tx_gas`].
125    MaxTxGas,
126}
127
128impl ConfigParamName {
129    /// Every parameter this build names, in protocol id order.
130    pub const ALL: &[Self] = &[
131        Self::MaxSha256Buffers,
132        Self::MaxCrossInvokeDepth,
133        Self::MaxNewContractsPerTx,
134        Self::MaxBytestringLen,
135        Self::MaxContractSizeBytes,
136        Self::MaxSavepointDepth,
137        Self::MaxAuthTokenDepth,
138        Self::MaxRv32CodeBytes,
139        Self::MaxRv32MemoryBytes,
140        Self::MaxTxGas,
141    ];
142
143    /// The protocol's field name for this parameter.
144    pub const fn as_str(self) -> &'static str {
145        match self {
146            Self::MaxSha256Buffers => "max_sha256_buffers",
147            Self::MaxCrossInvokeDepth => "max_cross_invoke_depth",
148            Self::MaxNewContractsPerTx => "max_new_contracts_per_tx",
149            Self::MaxBytestringLen => "max_bytestring_len",
150            Self::MaxContractSizeBytes => "max_contract_size_bytes",
151            Self::MaxSavepointDepth => "max_savepoint_depth",
152            Self::MaxAuthTokenDepth => "max_auth_token_depth",
153            Self::MaxRv32CodeBytes => "max_rv32_code_bytes",
154            Self::MaxRv32MemoryBytes => "max_rv32_memory_bytes",
155            Self::MaxTxGas => "max_tx_gas",
156        }
157    }
158
159    /// The value `params` states for this parameter, `None` when unstated.
160    ///
161    /// This is the binding between a name and the field it reads, so a
162    /// parameter added to one has to be added to the other.
163    pub fn stated_in(self, params: &NetworkConfigParams) -> Option<u64> {
164        match self {
165            Self::MaxSha256Buffers => params.max_sha256_buffers.map(u64::from),
166            Self::MaxCrossInvokeDepth => params.max_cross_invoke_depth.map(u64::from),
167            Self::MaxNewContractsPerTx => params.max_new_contracts_per_tx.map(u64::from),
168            Self::MaxBytestringLen => params.max_bytestring_len.map(u64::from),
169            Self::MaxContractSizeBytes => params.max_contract_size_bytes.map(u64::from),
170            Self::MaxSavepointDepth => params.max_savepoint_depth.map(u64::from),
171            Self::MaxAuthTokenDepth => params.max_auth_token_depth.map(u64::from),
172            Self::MaxRv32CodeBytes => params.max_rv32_code_bytes.map(u64::from),
173            Self::MaxRv32MemoryBytes => params.max_rv32_memory_bytes.map(u64::from),
174            Self::MaxTxGas => params.max_tx_gas,
175        }
176    }
177
178    /// The names this build knows, comma-separated, for diagnostics.
179    pub fn known_names() -> String {
180        Self::ALL
181            .iter()
182            .copied()
183            .map(Self::as_str)
184            .collect::<Vec<_>>()
185            .join(", ")
186    }
187}
188
189impl std::fmt::Display for ConfigParamName {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        f.write_str(self.as_str())
192    }
193}
194
195impl std::str::FromStr for ConfigParamName {
196    type Err = UnknownConfigParamName;
197
198    fn from_str(name: &str) -> Result<Self, Self::Err> {
199        Self::ALL
200            .iter()
201            .copied()
202            .find(|param| param.as_str() == name)
203            .ok_or_else(|| UnknownConfigParamName {
204                name: name.to_string(),
205            })
206    }
207}
208
209/// A name that does not belong to any parameter this build knows.
210#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
211#[error(
212    "unknown network parameter {name:?}; this build knows {}",
213    ConfigParamName::known_names()
214)]
215pub struct UnknownConfigParamName {
216    /// The name as it was written.
217    pub name: String,
218}
219
220/// Check a decoded transaction against the executor's pre-execution bounds.
221///
222/// The root invocation's `gas_limit` is checked first, against the
223/// `max_tx_gas` in force (1,000,000,000 when unstated), as hogd checks it
224/// before any attachment. The duplicate-attachment rule always applies. The attached-contract
225/// count is checked only when `params` states a bound for it. Each
226/// attached contract's code is checked against the size in force
227/// (`kDefaultMaxContractSizeBytes` when unstated), and each attached RV32
228/// artifact's declared guest memory and code size against
229/// `kDefaultMaxRv32MemoryBytes` and `kDefaultMaxRv32CodeBytes` when
230/// unstated, so pass `NetworkConfigParams::default()` when no chain
231/// reading is available and the protocol's own values apply.
232pub fn check_transaction(
233    tx: &Transaction,
234    params: &NetworkConfigParams,
235) -> Result<(), TxInvalidDetail> {
236    let max_gas = max_tx_gas_in_force(params);
237    if tx.invocation.gas_limit > max_gas {
238        return Err(TxInvalidDetail::GasLimitTooHigh {
239            gas_limit: tx.invocation.gas_limit,
240            max_gas,
241        });
242    }
243
244    let contracts = &tx.contracts_to_deploy.0;
245    let attached = u32::try_from(contracts.len()).unwrap_or(u32::MAX);
246
247    if let Some(max) = params.max_new_contracts_per_tx
248        && attached > max
249    {
250        return Err(TxInvalidDetail::TooManyContracts { attached, max });
251    }
252
253    let max_bytes = max_contract_size_bytes_in_force(params);
254    for (index, contract) in contracts.iter().enumerate() {
255        let index = u32::try_from(index).unwrap_or(u32::MAX);
256        let size_bytes = u32::try_from(contract_code_size(contract)).unwrap_or(u32::MAX);
257        if size_bytes > max_bytes {
258            return Err(TxInvalidDetail::ContractTooLarge {
259                index,
260                size_bytes,
261                max_bytes,
262            });
263        }
264    }
265
266    // The executor keys attachments by the SHA-256 of their XDR encoding, and
267    // TXS encoding is canonical, so value equality is hash equality. The
268    // attachment count is small (bounded by `max_new_contracts_per_tx`), so a
269    // pairwise scan beats hashing every blob on the submit path.
270    for (index, contract) in contracts.iter().enumerate() {
271        if let Some(first_index) = contracts[..index].iter().position(|seen| seen == contract) {
272            return Err(TxInvalidDetail::DuplicateContract {
273                index: u32::try_from(index).unwrap_or(u32::MAX),
274                first_index: u32::try_from(first_index).unwrap_or(u32::MAX),
275            });
276        }
277    }
278
279    // hogd verifies each attachment after the gates above, and its artifact
280    // parser checks the declared memory before the code size; the same order
281    // here, so both name the same first violation.
282    let max_memory_bytes = max_rv32_memory_bytes_in_force(params);
283    let max_code_bytes = max_rv32_code_bytes_in_force(params);
284    for (index, contract) in contracts.iter().enumerate() {
285        let Some(sizes) = rv32_declared_sizes(contract) else {
286            continue;
287        };
288        let index = u32::try_from(index).unwrap_or(u32::MAX);
289        if sizes.memory_bytes > max_memory_bytes {
290            return Err(TxInvalidDetail::Rv32MemoryTooLarge {
291                index,
292                memory_bytes: sizes.memory_bytes,
293                max_bytes: max_memory_bytes,
294            });
295        }
296        if sizes.code_bytes > max_code_bytes {
297            return Err(TxInvalidDetail::Rv32CodeTooLarge {
298                index,
299                code_bytes: sizes.code_bytes,
300                max_bytes: max_code_bytes,
301            });
302        }
303    }
304
305    Ok(())
306}
307
308/// The sizes an attached RV32 artifact's header declares, when it carries one.
309///
310/// A blob without a readable v5 header is left to the executor, which rejects
311/// it as unverifiable: this pre-check speaks only for headers it can read, and
312/// a contract shape this build does not know carries none.
313fn rv32_declared_sizes(contract: &Contract) -> Option<DeclaredSizes> {
314    match contract {
315        Contract::kRv32 { rv32_contract } => declared_sizes(&rv32_contract.data.0),
316        _ => None,
317    }
318}
319
320/// Code bytes the executor charges an attached contract for.
321///
322/// Mirrors hogd's `contract_blob_size`: only RV32 blobs carry code, and any
323/// contract shape this build does not know is treated as code-free so the
324/// executor, not this pre-check, decides its fate.
325fn contract_code_size(contract: &Contract) -> usize {
326    match contract {
327        Contract::kRv32 { rv32_contract } => rv32_contract.data.len(),
328        _ => 0,
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use thruput_xdr::{
335        contract::ContractBlob,
336        network_config::{ConfigParam, NetworkConfig, NetworkConfigV0},
337        precompiles::HogexecPrecompiledNativeContracts,
338        transaction::SignedTransaction,
339    };
340    use xdr_common::{BytesM, VecM};
341
342    use super::*;
343
344    /// Canonical signed-transaction vector shared with `json-rpc-server`'s
345    /// tests; only its inner transaction shape matters here.
346    const CANONICAL_SIGNED_TX_XDR: &str = "\
347bc10f38522c4e14265a1483af9e5375f5dea0e2c6bf25727ee8b429bf3c1a9b7\
348000000000000000000000000000f42400000000000000000000f424000000000000000010000000000000001\
34913df30e7ceeb7c4c78ba6287e9d8830cabf5a744f3d6be3139f2428d7bb4f9e7000000000000000000000001\
350000000000000000041d0144af2776cae9804ed899018a6bc17a5705d74c84e02556b9695031c9d3fe2554e5c\
351e62ceadc95ba43c4aa0884cd2f9ebea4e3845ef877761a6222e91f0d000000010000000050ec72dbafd4898c\
3525df0e15ea6ba3e46240945b7210fe2d63dde1e896f42107a";
353
354    fn transaction_with(contracts: Vec<Contract>) -> Transaction {
355        let canonical = hex::decode(CANONICAL_SIGNED_TX_XDR).unwrap();
356        let mut signed: SignedTransaction = xdrs::txs::decode(&canonical).unwrap();
357        signed.tx.contracts_to_deploy = VecM(contracts);
358        signed.tx
359    }
360
361    fn rv32(size: usize, fill: u8) -> Contract {
362        Contract::kRv32 {
363            rv32_contract: ContractBlob {
364                data: BytesM(vec![fill; size]),
365            },
366        }
367    }
368
369    fn network_config(params: Vec<ConfigParam>) -> NetworkConfig {
370        NetworkConfig::_0 {
371            v0: NetworkConfigV0 {
372                params: VecM(params),
373            },
374        }
375    }
376
377    /// The `ConfigParam` that states `value` for `name`.
378    ///
379    /// Exhaustive on purpose: a parameter added to [`ConfigParamName`] does
380    /// not compile here until it is given its union arm, and the test below
381    /// then fails until `from_network_config` lands it on its field.
382    fn stated(name: ConfigParamName, value: u32) -> ConfigParam {
383        match name {
384            ConfigParamName::MaxSha256Buffers => ConfigParam::kConfigParamMaxSHA256Buffers {
385                max_sha256_buffers: value,
386            },
387            ConfigParamName::MaxCrossInvokeDepth => ConfigParam::kConfigParamMaxCrossInvokeDepth {
388                max_cross_invoke_depth: value,
389            },
390            ConfigParamName::MaxNewContractsPerTx => {
391                ConfigParam::kConfigParamMaxNewContractsPerTx {
392                    max_new_contracts_per_tx: value,
393                }
394            }
395            ConfigParamName::MaxBytestringLen => ConfigParam::kConfigParamMaxBytestringLen {
396                max_bytestring_len: value,
397            },
398            ConfigParamName::MaxContractSizeBytes => {
399                ConfigParam::kConfigParamMaxContractSizeBytes {
400                    max_contract_size_bytes: value,
401                }
402            }
403            ConfigParamName::MaxSavepointDepth => ConfigParam::kConfigParamMaxSavepointDepth {
404                max_savepoint_depth: value,
405            },
406            ConfigParamName::MaxAuthTokenDepth => ConfigParam::kConfigParamMaxAuthTokenDepth {
407                max_auth_token_depth: value,
408            },
409            ConfigParamName::MaxRv32CodeBytes => ConfigParam::kConfigParamMaxRv32CodeBytes {
410                max_rv32_code_bytes: value,
411            },
412            ConfigParamName::MaxRv32MemoryBytes => ConfigParam::kConfigParamMaxRv32MemoryBytes {
413                max_rv32_memory_bytes: value,
414            },
415            ConfigParamName::MaxTxGas => ConfigParam::kConfigParamMaxTxGas {
416                max_tx_gas: u64::from(value),
417            },
418        }
419    }
420
421    #[test]
422    fn every_named_parameter_flattens_onto_the_field_it_names() {
423        for (name, value) in ConfigParamName::ALL.iter().copied().zip(1_000u32..) {
424            let params = NetworkConfigParams::from_network_config(&network_config(vec![stated(
425                name, value,
426            )]))
427            .unwrap();
428
429            assert_eq!(name.stated_in(&params), Some(u64::from(value)), "{name}");
430            for other in ConfigParamName::ALL.iter().copied() {
431                if other != name {
432                    assert_eq!(other.stated_in(&params), None, "{name} also stated {other}");
433                }
434            }
435        }
436    }
437
438    #[test]
439    fn names_round_trip_and_an_unknown_one_lists_the_known_names() {
440        for name in ConfigParamName::ALL.iter().copied() {
441            assert_eq!(name.as_str().parse::<ConfigParamName>(), Ok(name));
442            assert_eq!(name.to_string(), name.as_str());
443        }
444
445        let error = "max_rv32_memmory_bytes"
446            .parse::<ConfigParamName>()
447            .unwrap_err();
448
449        assert_eq!(error.name, "max_rv32_memmory_bytes");
450        assert!(
451            error.to_string().contains("max_rv32_memory_bytes"),
452            "{error}"
453        );
454    }
455
456    fn tx_gas_cap(max_tx_gas: u64) -> NetworkConfigParams {
457        NetworkConfigParams {
458            max_tx_gas: Some(max_tx_gas),
459            ..NetworkConfigParams::default()
460        }
461    }
462
463    fn transaction_budgeting(gas_limit: u64) -> Transaction {
464        let mut tx = transaction_with(vec![]);
465        tx.invocation.gas_limit = gas_limit;
466        tx
467    }
468
469    #[test]
470    fn root_gas_limit_is_bounded_by_the_default_cap_when_unstated() {
471        let params = NetworkConfigParams::default();
472        assert_eq!(
473            check_transaction(&transaction_budgeting(1_000_000_000), &params),
474            Ok(())
475        );
476        assert_eq!(
477            check_transaction(&transaction_budgeting(1_000_000_001), &params),
478            Err(TxInvalidDetail::GasLimitTooHigh {
479                gas_limit: 1_000_000_001,
480                max_gas: 1_000_000_000,
481            })
482        );
483        // The SDK's "all remaining gas" sentinel is no root budget.
484        assert_eq!(
485            check_transaction(&transaction_budgeting(u64::MAX), &params),
486            Err(TxInvalidDetail::GasLimitTooHigh {
487                gas_limit: u64::MAX,
488                max_gas: 1_000_000_000,
489            })
490        );
491    }
492
493    #[test]
494    fn root_gas_limit_is_bounded_by_the_stated_cap() {
495        // Lower than the default: the stated cap is the one in force.
496        assert_eq!(
497            check_transaction(&transaction_budgeting(500_001), &tx_gas_cap(500_000)),
498            Err(TxInvalidDetail::GasLimitTooHigh {
499                gas_limit: 500_001,
500                max_gas: 500_000,
501            })
502        );
503        // Higher than the default: a budget past 1B is admitted.
504        assert_eq!(
505            check_transaction(
506                &transaction_budgeting(5_000_000_000),
507                &tx_gas_cap(5_000_000_000)
508            ),
509            Ok(())
510        );
511    }
512
513    #[test]
514    fn gas_limit_is_checked_before_attachments() {
515        // hogd refuses the budget before it looks at any attachment, so an
516        // over-budget transaction that also attaches too many contracts is
517        // reported for its gas.
518        let mut tx = transaction_with(vec![rv32(8, 1), rv32(8, 2)]);
519        tx.invocation.gas_limit = 1_000_000_001;
520        assert_eq!(
521            check_transaction(&tx, &max_contracts(1)),
522            Err(TxInvalidDetail::GasLimitTooHigh {
523                gas_limit: 1_000_000_001,
524                max_gas: 1_000_000_000,
525            })
526        );
527    }
528
529    fn max_contracts(max_new_contracts_per_tx: u32) -> NetworkConfigParams {
530        NetworkConfigParams {
531            max_new_contracts_per_tx: Some(max_new_contracts_per_tx),
532            ..NetworkConfigParams::default()
533        }
534    }
535
536    fn contract_size_cap(max_contract_size_bytes: u32) -> NetworkConfigParams {
537        NetworkConfigParams {
538            max_contract_size_bytes: Some(max_contract_size_bytes),
539            ..NetworkConfigParams::default()
540        }
541    }
542
543    fn rv32_caps(
544        max_rv32_code_bytes: Option<u32>,
545        max_rv32_memory_bytes: Option<u32>,
546    ) -> NetworkConfigParams {
547        NetworkConfigParams {
548            max_rv32_code_bytes,
549            max_rv32_memory_bytes,
550            ..NetworkConfigParams::default()
551        }
552    }
553
554    /// A well-formed v5 artifact carrying `code_bytes` of code and declaring
555    /// `memory_bytes` of guest memory, as an attached contract.
556    fn artifact(code_bytes: usize, memory_bytes: usize) -> Contract {
557        let artifact =
558            thruput_rv32_artifact::build_artifact(&vec![0u8; code_bytes], &[], 0, memory_bytes)
559                .unwrap();
560        Contract::kRv32 {
561            rv32_contract: ContractBlob {
562                data: BytesM(artifact),
563            },
564        }
565    }
566
567    #[test]
568    fn every_stated_parameter_lands_on_its_field() {
569        let config = network_config(vec![
570            ConfigParam::kConfigParamMaxSHA256Buffers {
571                max_sha256_buffers: 3,
572            },
573            ConfigParam::kConfigParamMaxCrossInvokeDepth {
574                max_cross_invoke_depth: 17,
575            },
576            ConfigParam::kConfigParamMaxNewContractsPerTx {
577                max_new_contracts_per_tx: 7,
578            },
579            ConfigParam::kConfigParamMaxBytestringLen {
580                max_bytestring_len: 4096,
581            },
582            ConfigParam::kConfigParamMaxContractSizeBytes {
583                max_contract_size_bytes: 131_072,
584            },
585            ConfigParam::kConfigParamMaxSavepointDepth {
586                max_savepoint_depth: 9,
587            },
588            ConfigParam::kConfigParamMaxAuthTokenDepth {
589                max_auth_token_depth: 5,
590            },
591            ConfigParam::kConfigParamMaxRv32CodeBytes {
592                max_rv32_code_bytes: 65536,
593            },
594            ConfigParam::kConfigParamMaxRv32MemoryBytes {
595                max_rv32_memory_bytes: 1_048_576,
596            },
597            ConfigParam::kConfigParamMaxAuthTokens {
598                max_auth_tokens: 64,
599            },
600            ConfigParam::kConfigParamMaxCalldataLen {
601                max_calldata_len: 65_536,
602            },
603            ConfigParam::kConfigParamMaxReturnBufferLen {
604                max_return_buffer_len: 1024,
605            },
606            ConfigParam::kConfigParamMaxTxGas {
607                max_tx_gas: 5_000_000_000,
608            },
609        ]);
610
611        let params = NetworkConfigParams::from_network_config(&config).unwrap();
612
613        assert_eq!(
614            params,
615            NetworkConfigParams {
616                max_sha256_buffers: Some(3),
617                max_cross_invoke_depth: Some(17),
618                max_new_contracts_per_tx: Some(7),
619                max_bytestring_len: Some(4096),
620                max_contract_size_bytes: Some(131_072),
621                max_savepoint_depth: Some(9),
622                max_auth_token_depth: Some(5),
623                max_rv32_code_bytes: Some(65536),
624                max_rv32_memory_bytes: Some(1_048_576),
625                max_tx_gas: Some(5_000_000_000),
626            }
627        );
628    }
629
630    #[test]
631    fn unstated_parameters_stay_unset() {
632        // A list that stops after the second id is what a chain predating the
633        // later parameters states; nothing is invented for the rest.
634        let config = network_config(vec![
635            ConfigParam::kConfigParamMaxSHA256Buffers {
636                max_sha256_buffers: 8,
637            },
638            ConfigParam::kConfigParamMaxCrossInvokeDepth {
639                max_cross_invoke_depth: 64,
640            },
641        ]);
642
643        let params = NetworkConfigParams::from_network_config(&config).unwrap();
644
645        assert_eq!(
646            params,
647            NetworkConfigParams {
648                max_sha256_buffers: Some(8),
649                max_cross_invoke_depth: Some(64),
650                ..NetworkConfigParams::default()
651            }
652        );
653        assert_eq!(
654            NetworkConfigParams::from_network_config(&network_config(vec![])).unwrap(),
655            NetworkConfigParams::default()
656        );
657    }
658
659    #[test]
660    fn contract_size_in_force_is_the_stated_value_or_the_protocol_default() {
661        assert_eq!(
662            max_contract_size_bytes_in_force(&NetworkConfigParams::default()),
663            kDefaultMaxContractSizeBytes
664        );
665        assert_eq!(
666            max_contract_size_bytes_in_force(&contract_size_cap(1024)),
667            1024
668        );
669    }
670
671    #[test]
672    fn transaction_within_bounds_passes() {
673        let tx = transaction_with(vec![
674            rv32(16, 1),
675            rv32(kDefaultMaxContractSizeBytes as usize, 2),
676        ]);
677
678        assert_eq!(check_transaction(&tx, &max_contracts(2)), Ok(()));
679        assert_eq!(
680            check_transaction(&tx, &NetworkConfigParams::default()),
681            Ok(())
682        );
683    }
684
685    #[test]
686    fn too_many_contracts_is_rejected_only_when_the_bound_is_stated() {
687        let tx = transaction_with(vec![rv32(1, 1), rv32(1, 2), rv32(1, 3)]);
688
689        assert_eq!(
690            check_transaction(&tx, &max_contracts(2)),
691            Err(TxInvalidDetail::TooManyContracts {
692                attached: 3,
693                max: 2
694            })
695        );
696        assert_eq!(
697            check_transaction(&tx, &NetworkConfigParams::default()),
698            Ok(())
699        );
700    }
701
702    #[test]
703    fn oversized_contract_is_rejected_under_the_protocol_default() {
704        let tx = transaction_with(vec![
705            rv32(1, 1),
706            rv32(kDefaultMaxContractSizeBytes as usize + 1, 2),
707        ]);
708
709        assert_eq!(
710            check_transaction(&tx, &NetworkConfigParams::default()),
711            Err(TxInvalidDetail::ContractTooLarge {
712                index: 1,
713                size_bytes: kDefaultMaxContractSizeBytes + 1,
714                max_bytes: kDefaultMaxContractSizeBytes,
715            })
716        );
717    }
718
719    #[test]
720    fn stated_contract_size_cap_replaces_the_protocol_default() {
721        // A stated cap governs in both directions: below the default it
722        // rejects what the default would admit, above it admits what the
723        // default would reject.
724        let small = transaction_with(vec![rv32(1025, 1)]);
725        let large = transaction_with(vec![rv32(kDefaultMaxContractSizeBytes as usize + 1, 1)]);
726
727        assert_eq!(
728            check_transaction(&small, &contract_size_cap(1024)),
729            Err(TxInvalidDetail::ContractTooLarge {
730                index: 0,
731                size_bytes: 1025,
732                max_bytes: 1024,
733            })
734        );
735        assert_eq!(
736            check_transaction(&large, &contract_size_cap(kDefaultMaxContractSizeBytes + 1)),
737            Ok(())
738        );
739    }
740
741    #[test]
742    fn count_check_runs_before_size_check() {
743        // Cheapest check first: the count is knowable without touching any
744        // blob, and the executor orders them the same way.
745        let tx = transaction_with(vec![rv32(kDefaultMaxContractSizeBytes as usize + 1, 1); 2]);
746
747        assert!(matches!(
748            check_transaction(&tx, &max_contracts(1)),
749            Err(TxInvalidDetail::TooManyContracts { .. })
750        ));
751    }
752
753    #[test]
754    fn duplicate_contract_reports_both_positions() {
755        let tx = transaction_with(vec![rv32(4, 1), rv32(4, 2), rv32(4, 1)]);
756
757        assert_eq!(
758            check_transaction(&tx, &NetworkConfigParams::default()),
759            Err(TxInvalidDetail::DuplicateContract {
760                index: 2,
761                first_index: 0,
762            })
763        );
764    }
765
766    #[test]
767    fn rv32_bounds_in_force_are_the_stated_values_or_the_protocol_defaults() {
768        let unstated = NetworkConfigParams::default();
769        assert_eq!(max_rv32_code_bytes_in_force(&unstated), 262_144);
770        assert_eq!(max_rv32_memory_bytes_in_force(&unstated), 65_536);
771
772        let stated = rv32_caps(Some(4096), Some(1 << 20));
773        assert_eq!(max_rv32_code_bytes_in_force(&stated), 4096);
774        assert_eq!(max_rv32_memory_bytes_in_force(&stated), 1 << 20);
775    }
776
777    #[test]
778    fn rv32_sizes_at_the_protocol_defaults_pass() {
779        let tx = transaction_with(vec![artifact(DEFAULT_MAX_CODE_BYTES, DEFAULT_MEMORY_BYTES)]);
780
781        assert_eq!(
782            check_transaction(&tx, &NetworkConfigParams::default()),
783            Ok(())
784        );
785    }
786
787    #[test]
788    fn rv32_memory_over_the_bound_is_rejected_and_checked_before_code() {
789        // Both fields over: memory is reported, as hogd's artifact parser
790        // orders the two checks.
791        let tx = transaction_with(vec![
792            artifact(16, 1),
793            artifact(DEFAULT_MAX_CODE_BYTES + 4, DEFAULT_MEMORY_BYTES + 1),
794        ]);
795
796        assert_eq!(
797            check_transaction(&tx, &NetworkConfigParams::default()),
798            Err(TxInvalidDetail::Rv32MemoryTooLarge {
799                index: 1,
800                memory_bytes: 65_537,
801                max_bytes: 65_536,
802            })
803        );
804    }
805
806    #[test]
807    fn rv32_code_over_the_bound_is_rejected() {
808        let tx = transaction_with(vec![artifact(
809            DEFAULT_MAX_CODE_BYTES + 4,
810            DEFAULT_MEMORY_BYTES,
811        )]);
812
813        assert_eq!(
814            check_transaction(&tx, &NetworkConfigParams::default()),
815            Err(TxInvalidDetail::Rv32CodeTooLarge {
816                index: 0,
817                code_bytes: 262_148,
818                max_bytes: 262_144,
819            })
820        );
821    }
822
823    #[test]
824    fn stated_rv32_bounds_replace_the_protocol_defaults() {
825        // Above the defaults, a chain configured for it admits the artifact.
826        let large = transaction_with(vec![artifact(
827            DEFAULT_MAX_CODE_BYTES + 4,
828            DEFAULT_MEMORY_BYTES + 1,
829        )]);
830        assert_eq!(
831            check_transaction(&large, &rv32_caps(Some(262_148), Some(65_537))),
832            Ok(())
833        );
834
835        // Below the defaults, a stated cap rejects what the default admits.
836        let small = transaction_with(vec![artifact(1024, 4096)]);
837        assert_eq!(
838            check_transaction(&small, &rv32_caps(Some(1020), None)),
839            Err(TxInvalidDetail::Rv32CodeTooLarge {
840                index: 0,
841                code_bytes: 1024,
842                max_bytes: 1020,
843            })
844        );
845        assert_eq!(
846            check_transaction(&small, &rv32_caps(None, Some(4095))),
847            Err(TxInvalidDetail::Rv32MemoryTooLarge {
848                index: 0,
849                memory_bytes: 4096,
850                max_bytes: 4095,
851            })
852        );
853    }
854
855    #[test]
856    fn a_blob_without_an_artifact_header_is_left_to_the_executor() {
857        // Not a v5 artifact: the executor rejects it as unverifiable, and this
858        // pre-check has no header to read a declared size from.
859        let blob = rv32(DEFAULT_MAX_CODE_BYTES + 1024, 0x5a);
860        assert_eq!(rv32_declared_sizes(&blob), None);
861
862        let tx = transaction_with(vec![blob]);
863        assert_eq!(
864            check_transaction(&tx, &rv32_caps(Some(16), Some(16))),
865            Ok(())
866        );
867    }
868
869    #[test]
870    fn native_contracts_count_but_carry_no_code() {
871        let native = Contract::kNative {
872            native_contract_id: HogexecPrecompiledNativeContracts::from(0),
873        };
874        let tx = transaction_with(vec![native.clone(), rv32(4, 1)]);
875
876        assert_eq!(contract_code_size(&native), 0);
877        assert_eq!(
878            check_transaction(&tx, &max_contracts(1)),
879            Err(TxInvalidDetail::TooManyContracts {
880                attached: 2,
881                max: 1
882            })
883        );
884    }
885}