diff --git a/binary_port/src/error_code.rs b/binary_port/src/error_code.rs index 9b552d1f7b..6ebe361a3f 100644 --- a/binary_port/src/error_code.rs +++ b/binary_port/src/error_code.rs @@ -507,7 +507,7 @@ impl From for ErrorCode { InvalidTransactionV1::EntryPointCannotBeCall => { ErrorCode::InvalidTransactionEntryPointCannotBeCall } - InvalidTransactionV1::InvalidTransactionKind(_) => { + InvalidTransactionV1::InvalidTransactionLane(_) => { ErrorCode::InvalidTransactionInvalidTransactionKind } InvalidTransactionV1::GasPriceToleranceTooLow { .. } => { diff --git a/execution_engine/src/engine_state/mod.rs b/execution_engine/src/engine_state/mod.rs index 53789d57c8..43f5439ea4 100644 --- a/execution_engine/src/engine_state/mod.rs +++ b/execution_engine/src/engine_state/mod.rs @@ -23,7 +23,10 @@ pub use engine_config::{ }; pub use error::Error; use execution_kind::ExecutionKind; -pub use wasm_v1::{BlockInfo, ExecutableItem, InvalidRequest, WasmV1Request, WasmV1Result}; +pub use wasm_v1::{ + BlockInfo, ExecutableItem, InvalidRequest, SessionDataDeploy, SessionDataV1, SessionInputData, + WasmV1Request, WasmV1Result, +}; /// The maximum amount of motes that payment code execution can cost. pub const MAX_PAYMENT_AMOUNT: u64 = 2_500_000_000; diff --git a/execution_engine/src/engine_state/wasm_v1.rs b/execution_engine/src/engine_state/wasm_v1.rs index 428d534b99..640bdbb87d 100644 --- a/execution_engine/src/engine_state/wasm_v1.rs +++ b/execution_engine/src/engine_state/wasm_v1.rs @@ -1,7 +1,4 @@ -use std::{ - collections::BTreeSet, - convert::{TryFrom, TryInto}, -}; +use std::{collections::BTreeSet, convert::TryFrom}; use serde::Serialize; use thiserror::Error; @@ -10,14 +7,193 @@ use casper_storage::data_access_layer::TransferResult; use casper_types::{ account::AccountHash, bytesrepr::Bytes, contract_messages::Messages, execution::Effects, BlockHash, BlockTime, DeployHash, Digest, ExecutableDeployItem, Gas, InitiatorAddr, Phase, - PricingMode, RuntimeArgs, Transaction, TransactionCategory, TransactionEntryPoint, - TransactionHash, TransactionInvocationTarget, TransactionTarget, TransactionV1, Transfer, + PricingMode, RuntimeArgs, TransactionEntryPoint, TransactionHash, TransactionInvocationTarget, + TransactionTarget, TransactionV1Hash, Transfer, }; use crate::engine_state::{DeployItem, Error as EngineError}; const DEFAULT_ENTRY_POINT: &str = "call"; +/// Structure that needs to be filled with data so the engine can assemble wasm for deploy. +pub struct SessionDataDeploy<'a> { + deploy_hash: &'a DeployHash, + session: &'a ExecutableDeployItem, + initiator_addr: InitiatorAddr, + signers: BTreeSet, + is_standard_payment: bool, +} + +impl<'a> SessionDataDeploy<'a> { + /// Constructor + pub fn new( + deploy_hash: &'a DeployHash, + session: &'a ExecutableDeployItem, + initiator_addr: InitiatorAddr, + signers: BTreeSet, + is_standard_payment: bool, + ) -> Self { + Self { + deploy_hash, + session, + initiator_addr, + signers, + is_standard_payment, + } + } + + /// Deploy hash of the deploy + pub fn deploy_hash(&self) -> &DeployHash { + self.deploy_hash + } + + /// executable item of the deploy + pub fn session(&self) -> &ExecutableDeployItem { + self.session + } + + /// initiator address of the deploy + pub fn initiator_addr(&self) -> &InitiatorAddr { + &self.initiator_addr + } + + /// signers of the deploy + pub fn signers(&self) -> BTreeSet { + self.signers.clone() + } +} + +/// Structure that needs to be filled with data so the engine can assemble wasm for v1. +pub struct SessionDataV1<'a> { + args: &'a RuntimeArgs, + target: &'a TransactionTarget, + entry_point: &'a TransactionEntryPoint, + is_install_upgrade: bool, + hash: &'a TransactionV1Hash, + pricing_mode: &'a PricingMode, + initiator_addr: InitiatorAddr, + signers: BTreeSet, + is_standard_payment: bool, +} + +impl<'a> SessionDataV1<'a> { + #[allow(clippy::too_many_arguments)] + /// Constructor + pub fn new( + args: &'a RuntimeArgs, + target: &'a TransactionTarget, + entry_point: &'a TransactionEntryPoint, + is_install_upgrade: bool, + hash: &'a TransactionV1Hash, + pricing_mode: &'a PricingMode, + initiator_addr: InitiatorAddr, + signers: BTreeSet, + is_standard_payment: bool, + ) -> Self { + Self { + args, + target, + entry_point, + is_install_upgrade, + hash, + pricing_mode, + initiator_addr, + signers, + is_standard_payment, + } + } + + /// Runtime args passed with the transaction. + pub fn args(&self) -> &RuntimeArgs { + self.args + } + + /// Target of the transaction. + pub fn target(&self) -> &TransactionTarget { + self.target + } + + /// Entry point of the transaction + pub fn entry_point(&self) -> &TransactionEntryPoint { + self.entry_point + } + + /// Should session be allowed to perform install/upgrade operations + pub fn is_install_upgrade(&self) -> bool { + self.is_install_upgrade + } + + /// Hash of the transaction + pub fn hash(&self) -> &TransactionV1Hash { + self.hash + } + + /// initiator address of the transaction + pub fn initiator_addr(&self) -> &InitiatorAddr { + &self.initiator_addr + } + + /// signers of the transaction + pub fn signers(&self) -> BTreeSet { + self.signers.clone() + } + + /// Pricing mode of the transaction + pub fn pricing_mode(&self) -> &PricingMode { + self.pricing_mode + } +} + +/// Wrapper enum abstracting data for assmbling WasmV1Requests +pub enum SessionInputData<'a> { + /// Variant for sessions created from deploy transactions + DeploySessionData { + /// Deploy session data + data: SessionDataDeploy<'a>, + }, + /// Variant for sessions created from v1 transactions + SessionDataV1 { + /// v1 session data + data: SessionDataV1<'a>, + }, +} + +impl<'a> SessionInputData<'a> { + /// Transaction hash for the session + pub fn transaction_hash(&self) -> TransactionHash { + match self { + SessionInputData::DeploySessionData { data } => { + TransactionHash::Deploy(*data.deploy_hash()) + } + SessionInputData::SessionDataV1 { data } => TransactionHash::V1(*data.hash()), + } + } + + /// Initiator address for the session + pub fn initiator_addr(&self) -> &InitiatorAddr { + match self { + SessionInputData::DeploySessionData { data } => data.initiator_addr(), + SessionInputData::SessionDataV1 { data } => data.initiator_addr(), + } + } + + /// Signers for the session + pub fn signers(&self) -> BTreeSet { + match self { + SessionInputData::DeploySessionData { data } => data.signers(), + SessionInputData::SessionDataV1 { data } => data.signers(), + } + } + + /// determines if the transaction from which this session data was created is a standard payment + pub fn is_standard_payment(&self) -> bool { + match self { + SessionInputData::DeploySessionData { data } => data.is_standard_payment, + SessionInputData::SessionDataV1 { data } => data.is_standard_payment, + } + } +} + /// Error returned if constructing a new [`WasmV1Request`] fails. #[derive(Clone, Eq, PartialEq, Error, Serialize, Debug)] pub enum InvalidRequest { @@ -170,18 +346,12 @@ impl WasmV1Request { pub fn new_session( block_info: BlockInfo, gas_limit: Gas, - transaction: &Transaction, + session_input_data: &SessionInputData, ) -> Result { - let session_info = match transaction { - Transaction::Deploy(deploy) => { - SessionInfo::try_from((deploy.session(), deploy.hash()))? - } - Transaction::V1(v1_txn) => SessionInfo::try_from(v1_txn)?, - }; - - let transaction_hash = transaction.hash(); - let initiator_addr = transaction.initiator_addr(); - let authorization_keys = transaction.signers(); + let session_info = SessionInfo::try_from(session_input_data)?; + let transaction_hash = session_input_data.transaction_hash(); + let initiator_addr = session_input_data.initiator_addr().clone(); + let authorization_keys = session_input_data.signers().clone(); Ok(WasmV1Request::new_from_executable_info( block_info, gas_limit, @@ -196,18 +366,12 @@ impl WasmV1Request { pub fn new_custom_payment( block_info: BlockInfo, gas_limit: Gas, - transaction: &Transaction, + session_input_data: &SessionInputData, ) -> Result { - let payment_info = match transaction { - Transaction::Deploy(deploy) => { - PaymentInfo::try_from((deploy.payment(), deploy.hash()))? - } - Transaction::V1(v1_txn) => PaymentInfo::try_from(v1_txn)?, - }; - - let transaction_hash = transaction.hash(); - let initiator_addr = transaction.initiator_addr(); - let authorization_keys = transaction.signers(); + let payment_info = PaymentInfo::try_from(session_input_data)?; + let transaction_hash = session_input_data.transaction_hash(); + let initiator_addr = session_input_data.initiator_addr().clone(); + let authorization_keys = session_input_data.signers().clone(); Ok(WasmV1Request::new_from_executable_info( block_info, gas_limit, @@ -230,7 +394,8 @@ impl WasmV1Request { .. }: &DeployItem, ) -> Result { - let session_info = SessionInfo::try_from((session, deploy_hash))?; + let transaction_hash = TransactionHash::Deploy(*deploy_hash); + let session_info = build_session_info_for_executable_item(session, transaction_hash)?; let transaction_hash = TransactionHash::Deploy(*deploy_hash); let initiator_addr = InitiatorAddr::AccountHash(*address); let authorization_keys = authorization_keys.clone(); @@ -256,7 +421,8 @@ impl WasmV1Request { .. }: &DeployItem, ) -> Result { - let payment_info = PaymentInfo::try_from((payment, deploy_hash))?; + let transaction_hash = TransactionHash::Deploy(*deploy_hash); + let payment_info = build_payment_info_for_executable_item(payment, transaction_hash)?; let transaction_hash = TransactionHash::Deploy(*deploy_hash); let initiator_addr = InitiatorAddr::AccountHash(*address); let authorization_keys = authorization_keys.clone(); @@ -446,88 +612,117 @@ impl Executable for SessionInfo { } } -impl TryFrom<(&ExecutableDeployItem, &DeployHash)> for SessionInfo { +impl TryFrom<&SessionInputData<'_>> for PaymentInfo { type Error = InvalidRequest; - fn try_from( - (session_item, deploy_hash): (&ExecutableDeployItem, &DeployHash), - ) -> Result { - let transaction_hash = TransactionHash::Deploy(*deploy_hash); - let session: ExecutableItem; - let session_entry_point: String; - let session_args: RuntimeArgs; - match session_item { - ExecutableDeployItem::ModuleBytes { module_bytes, args } => { - session = ExecutableItem::LegacyDeploy(module_bytes.clone()); - session_entry_point = DEFAULT_ENTRY_POINT.to_string(); - session_args = args.clone(); - } - ExecutableDeployItem::StoredContractByHash { - hash, - entry_point, - args, - } => { - session = ExecutableItem::Invocation( - TransactionInvocationTarget::new_invocable_entity(*hash), - ); - session_entry_point = entry_point.clone(); - session_args = args.clone(); - } - ExecutableDeployItem::StoredContractByName { - name, - entry_point, - args, - } => { - session = ExecutableItem::Invocation( - TransactionInvocationTarget::new_invocable_entity_alias(name.clone()), - ); - session_entry_point = entry_point.clone(); - session_args = args.clone(); - } - ExecutableDeployItem::StoredVersionedContractByHash { - hash, - version, - entry_point, - args, - } => { - session = ExecutableItem::Invocation(TransactionInvocationTarget::new_package( - *hash, *version, - )); - session_entry_point = entry_point.clone(); - session_args = args.clone(); - } - ExecutableDeployItem::StoredVersionedContractByName { - name, - version, - entry_point, - args, - } => { - session = ExecutableItem::Invocation( - TransactionInvocationTarget::new_package_alias(name.clone(), *version), - ); - session_entry_point = entry_point.clone(); - session_args = args.clone(); - } - ExecutableDeployItem::Transfer { .. } => { - return Err(InvalidRequest::UnsupportedMode( - transaction_hash, - session_item.to_string(), - )); - } + fn try_from(input_data: &SessionInputData) -> Result { + match input_data { + SessionInputData::DeploySessionData { data } => PaymentInfo::try_from(data), + SessionInputData::SessionDataV1 { data } => PaymentInfo::try_from(data), + } + } +} + +impl TryFrom<&SessionInputData<'_>> for SessionInfo { + type Error = InvalidRequest; + + fn try_from(input_data: &SessionInputData) -> Result { + match input_data { + SessionInputData::DeploySessionData { data } => SessionInfo::try_from(data), + SessionInputData::SessionDataV1 { data } => SessionInfo::try_from(data), } + } +} + +impl TryFrom<&SessionDataDeploy<'_>> for SessionInfo { + type Error = InvalidRequest; + + fn try_from(deploy_data: &SessionDataDeploy) -> Result { + let transaction_hash = TransactionHash::Deploy(*deploy_data.deploy_hash()); + let session_item = deploy_data.session(); + build_session_info_for_executable_item(session_item, transaction_hash) + } +} - Ok(SessionInfo(ExecutableInfo { - item: session, - entry_point: session_entry_point, - args: session_args, - })) +fn build_session_info_for_executable_item( + session_item: &ExecutableDeployItem, + transaction_hash: TransactionHash, +) -> Result { + let session: ExecutableItem; + let session_entry_point: String; + let session_args: RuntimeArgs; + match session_item { + ExecutableDeployItem::ModuleBytes { module_bytes, args } => { + session = ExecutableItem::LegacyDeploy(module_bytes.clone()); + session_entry_point = DEFAULT_ENTRY_POINT.to_string(); + session_args = args.clone(); + } + ExecutableDeployItem::StoredContractByHash { + hash, + entry_point, + args, + } => { + session = ExecutableItem::Invocation( + TransactionInvocationTarget::new_invocable_entity(*hash), + ); + session_entry_point = entry_point.clone(); + session_args = args.clone(); + } + ExecutableDeployItem::StoredContractByName { + name, + entry_point, + args, + } => { + session = ExecutableItem::Invocation( + TransactionInvocationTarget::new_invocable_entity_alias(name.clone()), + ); + session_entry_point = entry_point.clone(); + session_args = args.clone(); + } + ExecutableDeployItem::StoredVersionedContractByHash { + hash, + version, + entry_point, + args, + } => { + session = ExecutableItem::Invocation(TransactionInvocationTarget::new_package( + *hash, *version, + )); + session_entry_point = entry_point.clone(); + session_args = args.clone(); + } + ExecutableDeployItem::StoredVersionedContractByName { + name, + version, + entry_point, + args, + } => { + session = ExecutableItem::Invocation(TransactionInvocationTarget::new_package_alias( + name.clone(), + *version, + )); + session_entry_point = entry_point.clone(); + session_args = args.clone(); + } + ExecutableDeployItem::Transfer { .. } => { + return Err(InvalidRequest::UnsupportedMode( + transaction_hash, + session_item.to_string(), + )); + } } + + Ok(SessionInfo(ExecutableInfo { + item: session, + entry_point: session_entry_point, + args: session_args, + })) } -impl TryFrom<&TransactionV1> for SessionInfo { +impl TryFrom<&SessionDataV1<'_>> for SessionInfo { type Error = InvalidRequest; - fn try_from(v1_txn: &TransactionV1) -> Result { + fn try_from(v1_txn: &SessionDataV1) -> Result { let transaction_hash = TransactionHash::V1(*v1_txn.hash()); let args = v1_txn.args().clone(); let session = match v1_txn.target() { @@ -558,28 +753,16 @@ impl TryFrom<&TransactionV1> for SessionInfo { v1_txn.entry_point().to_string(), )); }; - let category = v1_txn.transaction_category(); - let category: TransactionCategory = category.try_into().map_err(|_| { - InvalidRequest::InvalidCategory(transaction_hash, category.to_string()) - })?; - let item = match category { - TransactionCategory::InstallUpgrade => ExecutableItem::SessionBytes { - kind: SessionKind::InstallUpgradeBytecode, - module_bytes: module_bytes.clone(), - }, - TransactionCategory::Large - | TransactionCategory::Medium - | TransactionCategory::Small => ExecutableItem::SessionBytes { - kind: SessionKind::GenericBytecode, - module_bytes: module_bytes.clone(), - }, - _ => { - return Err(InvalidRequest::InvalidCategory( - transaction_hash, - category.to_string(), - )) - } + let kind = if v1_txn.is_install_upgrade() { + SessionKind::InstallUpgradeBytecode + } else { + SessionKind::GenericBytecode + }; + let item = ExecutableItem::SessionBytes { + kind, + module_bytes: module_bytes.clone(), }; + ExecutableInfo { item, entry_point: DEFAULT_ENTRY_POINT.to_owned(), @@ -613,85 +796,91 @@ impl Executable for PaymentInfo { } } -impl TryFrom<(&ExecutableDeployItem, &DeployHash)> for PaymentInfo { +impl TryFrom<&SessionDataDeploy<'_>> for PaymentInfo { type Error = InvalidRequest; - fn try_from( - (payment_item, deploy_hash): (&ExecutableDeployItem, &DeployHash), - ) -> Result { - let transaction_hash = TransactionHash::Deploy(*deploy_hash); - match payment_item { - ExecutableDeployItem::ModuleBytes { module_bytes, args } => { - let payment = if module_bytes.is_empty() { - return Err(InvalidRequest::UnsupportedMode( - transaction_hash, - "standard payment is no longer handled by the execution engine".to_string(), - )); - } else { - ExecutableItem::PaymentBytes(module_bytes.clone()) - }; - Ok(PaymentInfo(ExecutableInfo { - item: payment, - entry_point: DEFAULT_ENTRY_POINT.to_string(), - args: args.clone(), - })) - } - ExecutableDeployItem::StoredContractByHash { - hash, - args, - entry_point, - } => Ok(PaymentInfo(ExecutableInfo { - item: ExecutableItem::Invocation(TransactionInvocationTarget::ByHash(hash.value())), - entry_point: entry_point.clone(), - args: args.clone(), - })), - ExecutableDeployItem::StoredContractByName { - name, - args, - entry_point, - } => Ok(PaymentInfo(ExecutableInfo { - item: ExecutableItem::Invocation(TransactionInvocationTarget::ByName(name.clone())), - entry_point: entry_point.clone(), - args: args.clone(), - })), - ExecutableDeployItem::StoredVersionedContractByHash { - args, - hash, - version, - entry_point, - } => Ok(PaymentInfo(ExecutableInfo { - item: ExecutableItem::Invocation(TransactionInvocationTarget::ByPackageHash { - addr: hash.value(), - version: *version, - }), - entry_point: entry_point.clone(), - args: args.clone(), - })), - ExecutableDeployItem::StoredVersionedContractByName { - name, - version, - args, - entry_point, - } => Ok(PaymentInfo(ExecutableInfo { - item: ExecutableItem::Invocation(TransactionInvocationTarget::ByPackageName { - name: name.clone(), - version: *version, - }), - entry_point: entry_point.clone(), + fn try_from(deploy_data: &SessionDataDeploy) -> Result { + let payment_item = deploy_data.session(); + let transaction_hash = TransactionHash::Deploy(*deploy_data.deploy_hash()); + build_payment_info_for_executable_item(payment_item, transaction_hash) + } +} + +fn build_payment_info_for_executable_item( + payment_item: &ExecutableDeployItem, + transaction_hash: TransactionHash, +) -> Result { + match payment_item { + ExecutableDeployItem::ModuleBytes { module_bytes, args } => { + let payment = if module_bytes.is_empty() { + return Err(InvalidRequest::UnsupportedMode( + transaction_hash, + "standard payment is no longer handled by the execution engine".to_string(), + )); + } else { + ExecutableItem::PaymentBytes(module_bytes.clone()) + }; + Ok(PaymentInfo(ExecutableInfo { + item: payment, + entry_point: DEFAULT_ENTRY_POINT.to_string(), args: args.clone(), - })), - ExecutableDeployItem::Transfer { .. } => Err(InvalidRequest::UnexpectedVariant( - transaction_hash, - "payment item".to_string(), - )), + })) } + ExecutableDeployItem::StoredContractByHash { + hash, + args, + entry_point, + } => Ok(PaymentInfo(ExecutableInfo { + item: ExecutableItem::Invocation(TransactionInvocationTarget::ByHash(hash.value())), + entry_point: entry_point.clone(), + args: args.clone(), + })), + ExecutableDeployItem::StoredContractByName { + name, + args, + entry_point, + } => Ok(PaymentInfo(ExecutableInfo { + item: ExecutableItem::Invocation(TransactionInvocationTarget::ByName(name.clone())), + entry_point: entry_point.clone(), + args: args.clone(), + })), + ExecutableDeployItem::StoredVersionedContractByHash { + args, + hash, + version, + entry_point, + } => Ok(PaymentInfo(ExecutableInfo { + item: ExecutableItem::Invocation(TransactionInvocationTarget::ByPackageHash { + addr: hash.value(), + version: *version, + }), + entry_point: entry_point.clone(), + args: args.clone(), + })), + ExecutableDeployItem::StoredVersionedContractByName { + name, + version, + args, + entry_point, + } => Ok(PaymentInfo(ExecutableInfo { + item: ExecutableItem::Invocation(TransactionInvocationTarget::ByPackageName { + name: name.clone(), + version: *version, + }), + entry_point: entry_point.clone(), + args: args.clone(), + })), + ExecutableDeployItem::Transfer { .. } => Err(InvalidRequest::UnexpectedVariant( + transaction_hash, + "payment item".to_string(), + )), } } -impl TryFrom<&TransactionV1> for PaymentInfo { +impl TryFrom<&SessionDataV1<'_>> for PaymentInfo { type Error = InvalidRequest; - fn try_from(v1_txn: &TransactionV1) -> Result { + fn try_from(v1_txn: &SessionDataV1) -> Result { let transaction_hash = TransactionHash::V1(*v1_txn.hash()); match v1_txn.pricing_mode() { mode @ PricingMode::Classic { diff --git a/execution_engine_testing/test_support/src/execute_request_builder.rs b/execution_engine_testing/test_support/src/execute_request_builder.rs index 7960298077..45c98fdac5 100644 --- a/execution_engine_testing/test_support/src/execute_request_builder.rs +++ b/execution_engine_testing/test_support/src/execute_request_builder.rs @@ -1,12 +1,12 @@ use std::collections::BTreeSet; use casper_execution_engine::engine_state::{ - deploy_item::DeployItem, BlockInfo, ExecutableItem, WasmV1Request, + deploy_item::DeployItem, BlockInfo, ExecutableItem, SessionInputData, WasmV1Request, }; use casper_types::{ account::AccountHash, addressable_entity::DEFAULT_ENTRY_POINT_NAME, runtime_args, AddressableEntityHash, BlockHash, BlockTime, Digest, EntityVersion, Gas, InitiatorAddr, - PackageHash, Phase, RuntimeArgs, Transaction, TransactionHash, TransactionV1Hash, + PackageHash, Phase, RuntimeArgs, TransactionHash, TransactionV1Hash, }; use crate::{DeployItemBuilder, ARG_AMOUNT, DEFAULT_BLOCK_TIME, DEFAULT_PAYMENT}; @@ -50,19 +50,19 @@ impl ExecuteRequestBuilder { /// The default value used for `WasmV1Request::entry_point`. pub const DEFAULT_ENTRY_POINT: &'static str = "call"; - /// Converts a `Transaction` into an `ExecuteRequestBuilder`. - pub fn from_transaction(txn: &Transaction) -> Self { + /// Converts a `SessionInputData` into an `ExecuteRequestBuilder`. + pub fn from_session_input_data(session_input_data: &SessionInputData) -> Self { let block_info = BlockInfo::new( Self::DEFAULT_STATE_HASH, BlockTime::new(DEFAULT_BLOCK_TIME), BlockHash::default(), 0, ); - let authorization_keys = txn.authorization_keys(); + let authorization_keys = session_input_data.signers(); let session = WasmV1Request::new_session( block_info, Gas::new(5_000_000_000_000_u64), // TODO - set proper value - txn, + session_input_data, ) .unwrap(); @@ -70,7 +70,7 @@ impl ExecuteRequestBuilder { let payment_gas_limit: Gas; let payment_entry_point: String; let payment_args: RuntimeArgs; - if txn.is_standard_payment() { + if session_input_data.is_standard_payment() { payment = None; payment_gas_limit = Gas::zero(); payment_entry_point = DEFAULT_ENTRY_POINT_NAME.to_string(); @@ -85,7 +85,7 @@ impl ExecuteRequestBuilder { let request = WasmV1Request::new_custom_payment( block_info, Gas::new(5_000_000_000_000_u64), // TODO - set proper value - txn, + session_input_data, ) .unwrap(); payment = Some(request.executable_item); diff --git a/execution_engine_testing/tests/src/test/contract_api/add_contract_version.rs b/execution_engine_testing/tests/src/test/contract_api/add_contract_version.rs index ffc94a8817..3fca7d7280 100644 --- a/execution_engine_testing/tests/src/test/contract_api/add_contract_version.rs +++ b/execution_engine_testing/tests/src/test/contract_api/add_contract_version.rs @@ -2,15 +2,23 @@ use casper_engine_test_support::{ utils, ExecuteRequestBuilder, LmdbWasmTestBuilder, DEFAULT_ACCOUNT_ADDR, DEFAULT_ACCOUNT_SECRET_KEY, LOCAL_GENESIS_REQUEST, }; -use casper_execution_engine::{engine_state::Error as StateError, execution::ExecError}; +use casper_execution_engine::{ + engine_state::{Error as StateError, SessionDataDeploy, SessionDataV1, SessionInputData}, + execution::ExecError, +}; use casper_types::{ - ApiError, BlockTime, RuntimeArgs, Transaction, TransactionCategory, TransactionV1Builder, + ApiError, BlockTime, InitiatorAddr, Phase, PricingMode, RuntimeArgs, Transaction, + TransactionEntryPoint, TransactionTarget, TransactionV1Builder, }; const CONTRACT: &str = "do_nothing_stored.wasm"; const CHAIN_NAME: &str = "a"; const BLOCK_TIME: BlockTime = BlockTime::new(10); +pub(crate) const ARGS_MAP_KEY: u16 = 0; +pub(crate) const TARGET_MAP_KEY: u16 = 1; +pub(crate) const ENTRY_POINT_MAP_KEY: u16 = 2; + #[ignore] #[test] fn should_allow_add_contract_version_via_deploy() { @@ -24,24 +32,59 @@ fn should_allow_add_contract_version_via_deploy() { builder.exec(deploy_request).expect_success().commit(); } -fn try_add_contract_version(kind: TransactionCategory, should_succeed: bool) { +fn try_add_contract_version(is_install_upgrade: bool, should_succeed: bool) { let mut builder = LmdbWasmTestBuilder::default(); builder.run_genesis(LOCAL_GENESIS_REQUEST.clone()).commit(); let module_bytes = utils::read_wasm_file(CONTRACT); let txn = Transaction::from( - TransactionV1Builder::new_session(kind, module_bytes) + TransactionV1Builder::new_session(is_install_upgrade, module_bytes) .with_secret_key(&DEFAULT_ACCOUNT_SECRET_KEY) .with_chain_name(CHAIN_NAME) .build() .unwrap(), ); - - let txn_request = ExecuteRequestBuilder::from_transaction(&txn) - .with_block_time(BLOCK_TIME) - .build(); - + let txn_request = match txn { + Transaction::Deploy(ref deploy) => { + let initiator_addr = txn.initiator_addr(); + let is_standard_payment = deploy.payment().is_standard_payment(Phase::Payment); + let session_input_data = + to_deploy_session_input_data(is_standard_payment, initiator_addr, &txn); + ExecuteRequestBuilder::from_session_input_data(&session_input_data) + .with_block_time(BLOCK_TIME) + .build() + } + Transaction::V1(ref v1) => { + let initiator_addr = txn.initiator_addr(); + let is_standard_payment = if let PricingMode::Classic { + standard_payment, .. + } = v1.pricing_mode() + { + *standard_payment + } else { + true + }; + let args = v1.deserialize_field::(ARGS_MAP_KEY).unwrap(); + let target = v1 + .deserialize_field::(TARGET_MAP_KEY) + .unwrap(); + let entry_point = v1 + .deserialize_field::(ENTRY_POINT_MAP_KEY) + .unwrap(); + let session_input_data = to_v1_session_input_data( + is_standard_payment, + initiator_addr, + &args, + &target, + &entry_point, + &txn, + ); + ExecuteRequestBuilder::from_session_input_data(&session_input_data) + .with_block_time(BLOCK_TIME) + .build() + } + }; builder.exec(txn_request); if should_succeed { @@ -53,14 +96,69 @@ fn try_add_contract_version(kind: TransactionCategory, should_succeed: bool) { } } +fn to_deploy_session_input_data( + is_standard_payment: bool, + initiator_addr: InitiatorAddr, + txn: &Transaction, +) -> SessionInputData<'_> { + match txn { + Transaction::Deploy(deploy) => { + let data = SessionDataDeploy::new( + deploy.hash(), + deploy.session(), + initiator_addr, + txn.signers().clone(), + is_standard_payment, + ); + SessionInputData::DeploySessionData { data } + } + Transaction::V1(_) => { + panic!("unexpected transaction v1"); + } + } +} + +fn to_v1_session_input_data<'a>( + is_standard_payment: bool, + initiator_addr: InitiatorAddr, + args: &'a RuntimeArgs, + target: &'a TransactionTarget, + entry_point: &'a TransactionEntryPoint, + txn: &'a Transaction, +) -> SessionInputData<'a> { + let is_install_upgrade = match target { + TransactionTarget::Session { + is_install_upgrade, .. + } => *is_install_upgrade, + _ => false, + }; + match txn { + Transaction::Deploy(_) => panic!("unexpected deploy transaction"), + Transaction::V1(transaction_v1) => { + let data = SessionDataV1::new( + args, + target, + entry_point, + is_install_upgrade, + transaction_v1.hash(), + transaction_v1.pricing_mode(), + initiator_addr, + txn.signers().clone(), + is_standard_payment, + ); + SessionInputData::SessionDataV1 { data } + } + } +} + #[ignore] #[test] fn should_allow_add_contract_version_via_transaction_v1_installer_upgrader() { - try_add_contract_version(TransactionCategory::InstallUpgrade, true) + try_add_contract_version(true, true) } #[ignore] #[test] fn should_disallow_add_contract_version_via_transaction_v1_standard() { - try_add_contract_version(TransactionCategory::Large, false) + try_add_contract_version(false, false) } diff --git a/node/src/components/block_validator/state.rs b/node/src/components/block_validator/state.rs index b614110537..f6ed8b0e24 100644 --- a/node/src/components/block_validator/state.rs +++ b/node/src/components/block_validator/state.rs @@ -125,7 +125,7 @@ impl BlockValidationState { return (state, Some(responder)); } - if Self::validate_transaction_category_counts(proposed_block, &chainspec.transaction_config) + if Self::validate_transaction_lane_counts(proposed_block, &chainspec.transaction_config) .is_err() { let state = BlockValidationState::Invalid(proposed_block.timestamp()); @@ -187,15 +187,15 @@ impl BlockValidationState { (state, None) } - fn validate_transaction_category_counts( + fn validate_transaction_lane_counts( block: &ProposedBlock, config: &TransactionConfig, ) -> Result<(), ()> { - for supported_category in config.transaction_v1_config.get_supported_categories() { - let transactions = block.value().count(Some(supported_category)); + for supported_lane in config.transaction_v1_config.get_supported_lanes() { + let transactions = block.value().count(Some(supported_lane)); let lane_count_limit = config .transaction_v1_config - .get_max_transaction_count(supported_category); + .get_max_transaction_count(supported_lane); if lane_count_limit < transactions as u64 { warn!("too many transactions in category: {lane_count_limit}"); return Err(()); @@ -550,7 +550,7 @@ mod tests { use rand::Rng; use casper_types::{ - testing::TestRng, ChainspecRawBytes, TimeDiff, Transaction, TransactionHash, + testing::TestRng, ChainspecRawBytes, TimeDiff, Transaction, TransactionHash, TransactionV1, }; use super::{super::tests::*, *}; @@ -611,8 +611,8 @@ mod tests { let mut ret = vec![]; for _ in 0..auction_count { let txn = new_auction(self.rng, timestamp, ttl); - ret.push((TransactionHash::V1(*txn.hash()), txn.approvals().clone())); - self.transactions.push(Transaction::V1(txn)); + ret.push((txn.hash(), txn.approvals().clone())); + self.transactions.push(txn); } ret }; @@ -620,9 +620,11 @@ mod tests { let install_upgrade_for_block = { let mut ret = vec![]; for _ in 0..install_upgrade_count { - let txn = new_install_upgrade(self.rng, timestamp, ttl); - ret.push((TransactionHash::V1(*txn.hash()), txn.approvals().clone())); - self.transactions.push(Transaction::V1(txn)); + let txn: Transaction = + TransactionV1::random_install_upgrade(self.rng, Some(timestamp), Some(ttl)) + .into(); + ret.push((txn.hash(), txn.approvals().clone())); + self.transactions.push(txn); } ret }; diff --git a/node/src/components/block_validator/tests.rs b/node/src/components/block_validator/tests.rs index 9362230ae0..4ad1bd684f 100644 --- a/node/src/components/block_validator/tests.rs +++ b/node/src/components/block_validator/tests.rs @@ -8,8 +8,8 @@ use casper_types::{ bytesrepr::Bytes, runtime_args, system::standard_payment::ARG_AMOUNT, testing::TestRng, Block, BlockSignatures, BlockSignaturesV2, Chainspec, ChainspecRawBytes, Deploy, ExecutableDeployItem, FinalitySignatureV2, RuntimeArgs, SecretKey, TestBlockBuilder, TimeDiff, Transaction, - TransactionV1, TransactionV1Config, AUCTION_LANE_ID, INSTALL_UPGRADE_LANE_ID, MINT_LANE_ID, - U512, + TransactionHash, TransactionId, TransactionV1, TransactionV1Config, AUCTION_LANE_ID, + INSTALL_UPGRADE_LANE_ID, LARGE_WASM_LANE_ID, MINT_LANE_ID, U512, }; use crate::{ @@ -25,8 +25,6 @@ use crate::{ use super::*; -const LARGE_LANE_ID: u8 = 3; - #[derive(Debug, From)] enum ReactorEvent { #[from] @@ -154,7 +152,7 @@ pub(super) fn new_proposed_block_with_cited_signatures( INSTALL_UPGRADE_LANE_ID, install_upgrade.into_iter().collect(), ); - ret.insert(LARGE_LANE_ID, standard.into_iter().collect()); + ret.insert(LARGE_WASM_LANE_ID, standard.into_iter().collect()); ret }; let block_payload = BlockPayload::new(transactions, vec![], cited_signatures, true, 1u8); @@ -182,23 +180,29 @@ pub(super) fn new_v1_standard( rng: &mut TestRng, timestamp: Timestamp, ttl: TimeDiff, -) -> TransactionV1 { - TransactionV1::random_wasm(rng, Some(timestamp), Some(ttl)) +) -> Transaction { + let transaction_v1 = TransactionV1::random_wasm(rng, Some(timestamp), Some(ttl)); + Transaction::V1(transaction_v1) } -pub(super) fn new_auction(rng: &mut TestRng, timestamp: Timestamp, ttl: TimeDiff) -> TransactionV1 { - TransactionV1::random_auction(rng, Some(timestamp), Some(ttl)) +pub(super) fn new_auction(rng: &mut TestRng, timestamp: Timestamp, ttl: TimeDiff) -> Transaction { + let transaction_v1 = TransactionV1::random_auction(rng, Some(timestamp), Some(ttl)); + Transaction::V1(transaction_v1) } pub(super) fn new_install_upgrade( rng: &mut TestRng, timestamp: Timestamp, ttl: TimeDiff, -) -> TransactionV1 { - TransactionV1::random_install_upgrade(rng, Some(timestamp), Some(ttl)) +) -> Transaction { + TransactionV1::random_install_upgrade(rng, Some(timestamp), Some(ttl)).into() } -pub(super) fn new_legacy_deploy(rng: &mut TestRng, timestamp: Timestamp, ttl: TimeDiff) -> Deploy { +pub(super) fn new_legacy_deploy( + rng: &mut TestRng, + timestamp: Timestamp, + ttl: TimeDiff, +) -> Transaction { let secret_key = SecretKey::random(rng); let chain_name = "chain".to_string(); let payment = ExecutableDeployItem::ModuleBytes { @@ -223,21 +227,22 @@ pub(super) fn new_legacy_deploy(rng: &mut TestRng, timestamp: Timestamp, ttl: Ti &secret_key, None, ) + .into() } pub(super) fn new_v1_transfer( rng: &mut TestRng, timestamp: Timestamp, ttl: TimeDiff, -) -> TransactionV1 { - TransactionV1::random_transfer(rng, Some(timestamp), Some(ttl)) +) -> Transaction { + TransactionV1::random_transfer(rng, Some(timestamp), Some(ttl)).into() } pub(super) fn new_legacy_transfer( rng: &mut TestRng, timestamp: Timestamp, ttl: TimeDiff, -) -> Deploy { +) -> Transaction { let secret_key = SecretKey::random(rng); let chain_name = "chain".to_string(); let payment = ExecutableDeployItem::ModuleBytes { @@ -261,21 +266,22 @@ pub(super) fn new_legacy_transfer( &secret_key, None, ) + .into() } pub(super) fn new_mint(rng: &mut TestRng, timestamp: Timestamp, ttl: TimeDiff) -> Transaction { if rng.gen() { - new_v1_transfer(rng, timestamp, ttl).into() + new_v1_transfer(rng, timestamp, ttl) } else { - new_legacy_transfer(rng, timestamp, ttl).into() + new_legacy_transfer(rng, timestamp, ttl) } } pub(super) fn new_standard(rng: &mut TestRng, timestamp: Timestamp, ttl: TimeDiff) -> Transaction { if rng.gen() { - new_v1_standard(rng, timestamp, ttl).into() + new_v1_standard(rng, timestamp, ttl) } else { - new_legacy_deploy(rng, timestamp, ttl).into() + new_legacy_deploy(rng, timestamp, ttl) } } @@ -286,8 +292,8 @@ pub(super) fn new_non_transfer( ) -> Transaction { match rng.gen_range(0..3) { 0 => new_standard(rng, timestamp, ttl), - 1 => new_install_upgrade(rng, timestamp, ttl).into(), - 2 => new_auction(rng, timestamp, ttl).into(), + 1 => new_install_upgrade(rng, timestamp, ttl), + 2 => new_auction(rng, timestamp, ttl), _ => unreachable!(), } } @@ -795,8 +801,8 @@ async fn ttl() { new_non_transfer(&mut rng, 900.into(), ttl), ]; let transfers: Vec = vec![ - new_v1_transfer(&mut rng, 1000.into(), ttl).into(), - new_v1_transfer(&mut rng, 900.into(), ttl).into(), + new_v1_transfer(&mut rng, 1000.into(), ttl), + new_v1_transfer(&mut rng, 900.into(), ttl), ]; let mut transactions_context = ValidationContext::new() @@ -854,10 +860,10 @@ async fn transfer_transaction_mixup_and_replay() { let mut rng = TestRng::new(); let ttl = TimeDiff::from_millis(200); let timestamp = Timestamp::from(1000); - let deploy_legacy = Transaction::from(new_legacy_deploy(&mut rng, timestamp, ttl)); - let transaction_v1 = Transaction::from(new_v1_standard(&mut rng, timestamp, ttl)); - let transfer_legacy = Transaction::from(new_legacy_transfer(&mut rng, timestamp, ttl)); - let transfer_v1 = Transaction::from(new_v1_transfer(&mut rng, timestamp, ttl)); + let deploy_legacy = new_legacy_deploy(&mut rng, timestamp, ttl); + let transaction_v1 = new_v1_standard(&mut rng, timestamp, ttl); + let transfer_legacy = new_legacy_transfer(&mut rng, timestamp, ttl); + let transfer_v1 = new_v1_transfer(&mut rng, timestamp, ttl); // First we make sure that our transfers and transactions would normally be valid. let transactions = vec![deploy_legacy.clone(), transaction_v1.clone()]; @@ -985,7 +991,7 @@ async fn should_fetch_from_multiple_peers() { .map(|i| new_non_transfer(&mut rng, (900 + i).into(), ttl)) .collect_vec(); let transfers = (0..peer_count) - .map(|i| Transaction::V1(new_v1_transfer(&mut rng, (1000 + i).into(), ttl))) + .map(|i| new_v1_transfer(&mut rng, (1000 + i).into(), ttl)) .collect_vec(); // Assemble the block to be validated. @@ -1156,10 +1162,10 @@ async fn should_validate_block_with_signatures() { let mut rng = TestRng::new(); let ttl = TimeDiff::from_millis(200); let timestamp = Timestamp::from(1000); - let deploy_legacy = Transaction::from(new_legacy_deploy(&mut rng, timestamp, ttl)); - let transaction_v1 = Transaction::from(new_v1_standard(&mut rng, timestamp, ttl)); - let transfer_legacy = Transaction::from(new_legacy_transfer(&mut rng, timestamp, ttl)); - let transfer_v1 = Transaction::from(new_v1_transfer(&mut rng, timestamp, ttl)); + let deploy_legacy = new_legacy_deploy(&mut rng, timestamp, ttl); + let transaction_v1 = new_v1_standard(&mut rng, timestamp, ttl); + let transfer_legacy = new_legacy_transfer(&mut rng, timestamp, ttl); + let transfer_v1 = new_v1_transfer(&mut rng, timestamp, ttl); let context = ValidationContext::new() .with_num_validators(&mut rng, 3) @@ -1183,10 +1189,10 @@ async fn should_fetch_missing_signature() { let mut rng = TestRng::new(); let ttl = TimeDiff::from_millis(200); let timestamp = Timestamp::from(1000); - let deploy_legacy = Transaction::from(new_legacy_deploy(&mut rng, timestamp, ttl)); - let transaction_v1 = Transaction::from(new_v1_standard(&mut rng, timestamp, ttl)); - let transfer_legacy = Transaction::from(new_legacy_transfer(&mut rng, timestamp, ttl)); - let transfer_v1 = Transaction::from(new_v1_transfer(&mut rng, timestamp, ttl)); + let deploy_legacy = new_legacy_deploy(&mut rng, timestamp, ttl); + let transaction_v1 = new_v1_standard(&mut rng, timestamp, ttl); + let transfer_legacy = new_legacy_transfer(&mut rng, timestamp, ttl); + let transfer_v1 = new_v1_transfer(&mut rng, timestamp, ttl); let context = ValidationContext::new() .with_num_validators(&mut rng, 3) @@ -1213,10 +1219,10 @@ async fn should_fail_if_unable_to_fetch_signature() { let mut rng = TestRng::new(); let ttl = TimeDiff::from_millis(200); let timestamp = Timestamp::from(1000); - let deploy_legacy = Transaction::from(new_legacy_deploy(&mut rng, timestamp, ttl)); - let transaction_v1 = Transaction::from(new_v1_standard(&mut rng, timestamp, ttl)); - let transfer_legacy = Transaction::from(new_legacy_transfer(&mut rng, timestamp, ttl)); - let transfer_v1 = Transaction::from(new_v1_transfer(&mut rng, timestamp, ttl)); + let deploy_legacy = new_legacy_deploy(&mut rng, timestamp, ttl); + let transaction_v1 = new_v1_standard(&mut rng, timestamp, ttl); + let transfer_legacy = new_legacy_transfer(&mut rng, timestamp, ttl); + let transfer_v1 = new_v1_transfer(&mut rng, timestamp, ttl); let context = ValidationContext::new() .with_num_validators(&mut rng, 3) @@ -1263,10 +1269,10 @@ async fn should_validate_with_delayed_block() { let mut rng = TestRng::new(); let ttl = TimeDiff::from_millis(200); let timestamp = Timestamp::from(1000); - let deploy_legacy = Transaction::from(new_legacy_deploy(&mut rng, timestamp, ttl)); - let transaction_v1 = Transaction::from(new_v1_standard(&mut rng, timestamp, ttl)); - let transfer_legacy = Transaction::from(new_legacy_transfer(&mut rng, timestamp, ttl)); - let transfer_v1 = Transaction::from(new_v1_transfer(&mut rng, timestamp, ttl)); + let deploy_legacy = new_legacy_deploy(&mut rng, timestamp, ttl); + let transaction_v1 = new_v1_standard(&mut rng, timestamp, ttl); + let transfer_legacy = new_legacy_transfer(&mut rng, timestamp, ttl); + let transfer_v1 = new_v1_transfer(&mut rng, timestamp, ttl); let context = ValidationContext::new() .with_num_validators(&mut rng, 3) diff --git a/node/src/components/contract_runtime/error.rs b/node/src/components/contract_runtime/error.rs index 5e300836cd..bd35a17760 100644 --- a/node/src/components/contract_runtime/error.rs +++ b/node/src/components/contract_runtime/error.rs @@ -154,4 +154,6 @@ pub enum BlockExecutionError { #[error("Unsupported execution kind: {0}")] /// Unsupported execution kind UnsupportedTransactionKind(u8), + #[error("Error while converting transaction to internal representation: {0}")] + TransactionConversion(String), } diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index 2de7e9f0d1..b27b39832b 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -1,6 +1,5 @@ -use std::{collections::BTreeMap, convert::TryInto, sync::Arc, time::Instant}; - use itertools::Itertools; +use std::{collections::BTreeMap, convert::TryInto, sync::Arc, time::Instant}; use tracing::{debug, error, info, trace, warn}; use casper_execution_engine::engine_state::{ @@ -30,7 +29,7 @@ use casper_types::{ execution::{Effects, ExecutionResult, TransformKindV2, TransformV2}, system::handle_payment::ARG_AMOUNT, BlockHash, BlockHeader, BlockTime, BlockV2, CLValue, Chainspec, ChecksumRegistry, Digest, - EraEndV2, EraId, FeeHandling, Gas, GasLimited, Key, ProtocolVersion, PublicKey, RefundHandling, + EraEndV2, EraId, FeeHandling, Gas, Key, ProtocolVersion, PublicKey, RefundHandling, Transaction, AUCTION_LANE_ID, MINT_LANE_ID, U512, }; @@ -43,7 +42,7 @@ use super::{ use crate::{ components::fetcher::FetchItem, contract_runtime::types::ExecutionArtifactBuilder, - types::{self, Chunkable, ExecutableBlock, InternalEraReport}, + types::{self, Chunkable, ExecutableBlock, InternalEraReport, MetaTransaction}, }; /// Executes a finalized block. @@ -140,14 +139,17 @@ pub fn execute_finalized_block( } } - for transaction in executable_block.transactions { - let mut artifact_builder = ExecutionArtifactBuilder::new(&transaction); + let transaction_config = &chainspec.transaction_config; + for stored_transaction in executable_block.transactions { + let mut artifact_builder = ExecutionArtifactBuilder::new(&stored_transaction); + let transaction = MetaTransaction::from(&stored_transaction, transaction_config) + .map_err(|err| BlockExecutionError::TransactionConversion(err.to_string()))?; let initiator_addr = transaction.initiator_addr(); let transaction_hash = transaction.hash(); let runtime_args = transaction.session_args().clone(); let entry_point = transaction.entry_point(); - let authorization_keys = transaction.authorization_keys(); + let authorization_keys = transaction.signers(); /* we solve for halting state using a `gas limit` which is the maximum amount of @@ -170,19 +172,24 @@ pub fn execute_finalized_block( */ // NOTE: this is the allowed computation limit (gas limit) - let gas_limit = match transaction.gas_limit(chainspec) { - Ok(gas) => gas, - Err(ite) => { - debug!(%transaction_hash, %ite, "invalid transaction (gas limit)"); - artifact_builder.with_invalid_transaction(&ite); - artifacts.push(artifact_builder.build()); - continue; - } - }; + let gas_limit = + match stored_transaction.gas_limit(chainspec, transaction.transaction_lane()) { + Ok(gas) => gas, + Err(ite) => { + debug!(%transaction_hash, %ite, "invalid transaction (gas limit)"); + artifact_builder.with_invalid_transaction(&ite); + artifacts.push(artifact_builder.build()); + continue; + } + }; artifact_builder.with_gas_limit(gas_limit); // NOTE: this is the actual adjusted cost that we charge for (gas limit * gas price) - let cost = match transaction.gas_cost(chainspec, current_gas_price) { + let cost = match stored_transaction.gas_cost( + chainspec, + transaction.transaction_lane(), + current_gas_price, + ) { Ok(motes) => motes.value(), Err(ite) => { debug!(%transaction_hash, "invalid transaction (motes conversion)"); @@ -237,10 +244,11 @@ pub fn execute_finalized_block( // using a multiple of a small value. chainspec.transaction_config.native_transfer_minimum_motes * 5, ); + let session_input_data = transaction.to_session_input_data(); let pay_result = match WasmV1Request::new_custom_payment( BlockInfo::new(state_root_hash, block_time, parent_block_hash, block_height), custom_payment_gas_limit, - &transaction, + &session_input_data, ) { Ok(mut pay_request) => { // We'll send a hint to the custom payment logic on the amount @@ -280,12 +288,12 @@ pub fn execute_finalized_block( ProofHandling::NoProofs, )); - let category = transaction.transaction_category(); + let lane_id = transaction.transaction_lane(); let allow_execution = { let is_not_penalized = !balance_identifier.is_penalty(); let sufficient_balance = initial_balance_result.is_sufficient(cost); - let is_supported = chainspec.is_supported(category); + let is_supported = chainspec.is_supported(lane_id); trace!(%transaction_hash, ?sufficient_balance, ?is_not_penalized, ?is_supported, "payment preprocessing"); is_not_penalized && sufficient_balance && is_supported }; @@ -309,9 +317,9 @@ pub fn execute_finalized_block( .map_err(|_| BlockExecutionError::RootNotFound(state_root_hash))?; } - trace!(%transaction_hash, ?category, "eligible for execution"); - match category { - category if category == MINT_LANE_ID => { + trace!(%transaction_hash, ?lane_id, "eligible for execution"); + match lane_id { + lane_id if lane_id == MINT_LANE_ID => { let transfer_result = scratch_state.transfer(TransferRequest::with_runtime_args( native_runtime_config.clone(), @@ -330,7 +338,7 @@ pub fn execute_finalized_block( .with_transfer_result(transfer_result) .map_err(|_| BlockExecutionError::RootNotFound(state_root_hash))?; } - category if category == AUCTION_LANE_ID => { + lane_id if lane_id == AUCTION_LANE_ID => { match AuctionMethod::from_parts(entry_point, &runtime_args, chainspec) { Ok(auction_method) => { let bidding_result = scratch_state.bidding(BiddingRequest::new( @@ -364,6 +372,7 @@ pub fn execute_finalized_block( } _ => { let wasm_v1_start = Instant::now(); + let session_input_data = transaction.to_session_input_data(); match WasmV1Request::new_session( BlockInfo::new( state_root_hash, @@ -372,13 +381,13 @@ pub fn execute_finalized_block( block_height, ), gas_limit, - &transaction, + &session_input_data, ) { Ok(wasm_v1_request) => { - trace!(%transaction_hash, ?category, ?wasm_v1_request, "able to get wasm v1 request"); + trace!(%transaction_hash, ?lane_id, ?wasm_v1_request, "able to get wasm v1 request"); let wasm_v1_result = execution_engine_v1.execute(&scratch_state, wasm_v1_request); - trace!(%transaction_hash, ?category, ?wasm_v1_result, "able to get wasm v1 result"); + trace!(%transaction_hash, ?lane_id, ?wasm_v1_result, "able to get wasm v1 result"); state_root_hash = scratch_state.commit_effects( state_root_hash, wasm_v1_result.effects().clone(), @@ -389,7 +398,7 @@ pub fn execute_finalized_block( .map_err(|_| BlockExecutionError::RootNotFound(state_root_hash))?; } Err(ire) => { - debug!(%transaction_hash, ?category, ?ire, "unable to get wasm v1 request"); + debug!(%transaction_hash, ?lane_id, ?ire, "unable to get wasm v1 request"); artifact_builder.with_invalid_wasm_v1_request(&ire); } }; @@ -1017,21 +1026,27 @@ pub(super) fn speculatively_execute( chainspec: &Chainspec, execution_engine_v1: &ExecutionEngineV1, block_header: BlockHeader, - transaction: Transaction, + input_transaction: Transaction, ) -> SpeculativeExecutionResult where S: StateProvider, { + let transaction_config = &chainspec.transaction_config; + let maybe_transaction = MetaTransaction::from(&input_transaction, transaction_config); + if let Err(error) = maybe_transaction { + return SpeculativeExecutionResult::invalid_transaction(error); + } + let transaction = maybe_transaction.unwrap(); let state_root_hash = block_header.state_root_hash(); let parent_block_hash = block_header.block_hash(); let block_height = block_header.height(); let block_time = block_header .timestamp() .saturating_add(chainspec.core_config.minimum_block_time); - let gas_limit = match transaction.gas_limit(chainspec) { + let gas_limit = match input_transaction.gas_limit(chainspec, transaction.transaction_lane()) { Ok(gas_limit) => gas_limit, Err(_) => { - return SpeculativeExecutionResult::invalid_gas_limit(transaction); + return SpeculativeExecutionResult::invalid_gas_limit(input_transaction); } }; @@ -1066,8 +1081,9 @@ where parent_block_hash, block_height, ); + let session_input_data = transaction.to_session_input_data(); let wasm_v1_result = - match WasmV1Request::new_session(block_info, gas_limit, &transaction) { + match WasmV1Request::new_session(block_info, gas_limit, &session_input_data) { Ok(wasm_v1_request) => { execution_engine_v1.execute(state_provider, wasm_v1_request) } diff --git a/node/src/components/contract_runtime/types.rs b/node/src/components/contract_runtime/types.rs index bd65ddf60e..469ebc7318 100644 --- a/node/src/components/contract_runtime/types.rs +++ b/node/src/components/contract_runtime/types.rs @@ -1,5 +1,6 @@ use std::{collections::BTreeMap, sync::Arc}; +use crate::types::TransactionHeader; use casper_types::{execution::PaymentInfo, InitiatorAddr, Transfer}; use datasize::DataSize; use serde::Serialize; @@ -18,8 +19,7 @@ use casper_types::{ contract_messages::Messages, execution::{Effects, ExecutionResult, ExecutionResultV2}, BlockHash, BlockHeaderV2, BlockV2, Digest, EraId, Gas, InvalidDeploy, InvalidTransaction, - InvalidTransactionV1, ProtocolVersion, PublicKey, Transaction, TransactionHash, - TransactionHeader, U512, + InvalidTransactionV1, ProtocolVersion, PublicKey, Transaction, TransactionHash, U512, }; /// Request for validator weights for a specific era. @@ -83,7 +83,7 @@ impl ExecutionArtifactBuilder { ExecutionArtifactBuilder { effects: Effects::new(), hash: transaction.hash(), - header: transaction.header(), + header: transaction.into(), error_message: None, transfers: vec![], messages: Default::default(), @@ -387,6 +387,10 @@ impl SpeculativeExecutionResult { ), } } + + pub fn invalid_transaction(error: InvalidTransaction) -> Self { + SpeculativeExecutionResult::InvalidTransaction(error) + } } /// State to use to construct the next block in the blockchain. Includes the state root hash for the diff --git a/node/src/components/contract_runtime/utils.rs b/node/src/components/contract_runtime/utils.rs index 67f66f9837..46abc2c5be 100644 --- a/node/src/components/contract_runtime/utils.rs +++ b/node/src/components/contract_runtime/utils.rs @@ -35,9 +35,7 @@ use casper_storage::{ }, global_state::state::{lmdb::LmdbGlobalState, CommitProvider, StateProvider}, }; -use casper_types::{ - BlockHash, Chainspec, Digest, EraId, Gas, GasLimited, Key, ProtocolUpgradeConfig, -}; +use casper_types::{BlockHash, Chainspec, Digest, EraId, Gas, Key, ProtocolUpgradeConfig}; /// Maximum number of resource intensive tasks that can be run in parallel. /// @@ -137,12 +135,18 @@ pub(super) async fn exec_or_requeue( let switch_block_utilization_score = { let mut has_hit_slot_limt = false; + let mut transaction_hash_to_lane_id = HashMap::new(); - for (category, transactions) in executable_block.transaction_map.iter() { + for (lane_id, transactions) in executable_block.transaction_map.iter() { + transaction_hash_to_lane_id.extend( + transactions + .iter() + .map(|transaction| (transaction, *lane_id)), + ); let max_count = chainspec .transaction_config .transaction_v1_config - .get_max_transaction_count(*category); + .get_max_transaction_count(*lane_id); if max_count == transactions.len() as u64 { has_hit_slot_limt = true; } @@ -162,16 +166,25 @@ pub(super) async fn exec_or_requeue( Ratio::new(total_size_of_transactions * 100, max_block_size).to_integer() }; - let gas_utilization: u64 = { let total_gas_limit: u64 = executable_block .transactions .iter() - .map(|transaction| match transaction.gas_limit(&chainspec) { - Ok(gas_limit) => gas_limit.value().as_u64(), - Err(_) => { - warn!("Unable to determine gas limit"); - 0u64 + .map(|transaction| { + match transaction_hash_to_lane_id.get(&transaction.hash()) { + Some(lane_id) => { + match &transaction.gas_limit(&chainspec, *lane_id) { + Ok(gas_limit) => gas_limit.value().as_u64(), + Err(_) => { + warn!("Unable to determine gas limit"); + 0u64 + } + } + } + None => { + warn!("Unable to determine gas limit"); + 0u64 + } } }) .sum(); diff --git a/node/src/components/event_stream_server.rs b/node/src/components/event_stream_server.rs index ff7f0882bd..e8d9820e3c 100644 --- a/node/src/components/event_stream_server.rs +++ b/node/src/components/event_stream_server.rs @@ -35,13 +35,14 @@ use tokio::sync::{ use tracing::{error, info, warn}; use warp::Filter; -use casper_types::{InitiatorAddr, ProtocolVersion, TransactionHeader}; +use casper_types::{InitiatorAddr, ProtocolVersion}; use super::Component; use crate::{ components::{ComponentState, InitializedComponent, PortBoundComponent}, effect::{EffectBuilder, Effects}, reactor::main_reactor::MainEvent, + types::TransactionHeader, utils::{self, ListeningError}, NodeRng, }; @@ -304,10 +305,10 @@ where deploy_header.timestamp(), deploy_header.ttl(), ), - TransactionHeader::V1(txn_header) => ( - txn_header.initiator_addr().clone(), - txn_header.timestamp(), - txn_header.ttl(), + TransactionHeader::V1(metadata) => ( + metadata.initiator_addr().clone(), + metadata.timestamp(), + metadata.ttl(), ), }; self.broadcast(SseData::TransactionProcessed { diff --git a/node/src/components/event_stream_server/event.rs b/node/src/components/event_stream_server/event.rs index 7eec8d1b68..d9414ded2b 100644 --- a/node/src/components/event_stream_server/event.rs +++ b/node/src/components/event_stream_server/event.rs @@ -3,13 +3,13 @@ use std::{ sync::Arc, }; +use crate::types::TransactionHeader; use itertools::Itertools; use casper_types::{ contract_messages::Messages, execution::{Effects, ExecutionResult}, Block, BlockHash, EraId, FinalitySignature, PublicKey, Timestamp, Transaction, TransactionHash, - TransactionHeader, }; #[derive(Debug)] diff --git a/node/src/components/fetcher/fetcher_impls/transaction_fetcher.rs b/node/src/components/fetcher/fetcher_impls/transaction_fetcher.rs index 2b1bdc8e91..1e79c7a9c3 100644 --- a/node/src/components/fetcher/fetcher_impls/transaction_fetcher.rs +++ b/node/src/components/fetcher/fetcher_impls/transaction_fetcher.rs @@ -26,10 +26,7 @@ impl FetchItem for Transaction { } fn validate(&self, _metadata: &EmptyValidationMetadata) -> Result<(), Self::ValidationError> { - match self { - Transaction::Deploy(deploy) => deploy.is_valid().map_err(Into::into), - Transaction::V1(txn) => txn.verify().map_err(Into::into), - } + self.verify() } } diff --git a/node/src/components/storage.rs b/node/src/components/storage.rs index f3ccaeeb8a..1b530b8594 100644 --- a/node/src/components/storage.rs +++ b/node/src/components/storage.rs @@ -69,8 +69,7 @@ use casper_types::{ Approval, ApprovalsHash, AvailableBlockRange, Block, BlockBody, BlockHash, BlockHeader, BlockSignatures, BlockSignaturesV1, BlockSignaturesV2, BlockV2, ChainNameDigest, DeployHash, EraId, ExecutionInfo, FinalitySignature, ProtocolVersion, SignedBlockHeader, Timestamp, - Transaction, TransactionConfig, TransactionHash, TransactionHeader, TransactionId, Transfer, - U512, + Transaction, TransactionConfig, TransactionHash, TransactionId, Transfer, U512, }; use datasize::DataSize; use num_rational::Ratio; @@ -94,7 +93,7 @@ use crate::{ types::{ BlockExecutionResultsOrChunk, BlockExecutionResultsOrChunkId, BlockWithMetadata, ExecutableBlock, LegacyDeploy, MaxTtl, NodeId, NodeRng, SyncLeap, SyncLeapIdentifier, - VariantMismatch, + TransactionHeader, VariantMismatch, }, utils::{display_error, WithDir}, }; @@ -2016,11 +2015,9 @@ impl Storage { deploy.take_header().into(), execution_result, )), - Some(Transaction::V1(transaction_v1)) => ret.push(( - transaction_hash, - transaction_v1.take_header().into(), - execution_result, - )), + Some(Transaction::V1(transaction_v1)) => { + ret.push((transaction_hash, (&transaction_v1).into(), execution_result)) + } }; } Ok(Some(ret)) diff --git a/node/src/components/transaction_acceptor.rs b/node/src/components/transaction_acceptor.rs index 8abbf5b67f..dd331e3314 100644 --- a/node/src/components/transaction_acceptor.rs +++ b/node/src/components/transaction_acceptor.rs @@ -17,7 +17,7 @@ use casper_types::{ AddressableEntityHash, AddressableEntityIdentifier, BlockHeader, Chainspec, EntityAddr, EntityVersion, EntityVersionKey, EntryPoint, EntryPointAddr, ExecutableDeployItem, ExecutableDeployItemIdentifier, InitiatorAddr, Key, Package, PackageAddr, PackageHash, - PackageIdentifier, Transaction, TransactionEntryPoint, TransactionInvocationTarget, + PackageIdentifier, Timestamp, Transaction, TransactionEntryPoint, TransactionInvocationTarget, TransactionTarget, DEFAULT_ENTRY_POINT_NAME, U512, }; @@ -29,6 +29,7 @@ use crate::{ EffectBuilder, EffectExt, Effects, Responder, }, fatal, + types::MetaTransaction, utils::Source, NodeRng, }; @@ -105,29 +106,42 @@ impl TransactionAcceptor { fn accept( &mut self, effect_builder: EffectBuilder, - transaction: Transaction, + input_transaction: Transaction, source: Source, maybe_responder: Option>>, ) -> Effects { - debug!(%source, %transaction, "checking transaction before accepting"); - let event_metadata = Box::new(EventMetadata::new(transaction, source, maybe_responder)); - - let is_config_compliant = match &event_metadata.transaction { - Transaction::Deploy(deploy) => deploy - .is_config_compliant( - &self.chainspec, - self.acceptor_config.timestamp_leeway, - event_metadata.verification_start_timestamp, - ) - .map_err(|err| Error::InvalidTransaction(err.into())), - Transaction::V1(txn) => txn - .is_config_compliant( - &self.chainspec, - self.acceptor_config.timestamp_leeway, - event_metadata.verification_start_timestamp, - ) - .map_err(|err| Error::InvalidTransaction(err.into())), + debug!(%source, %input_transaction, "checking transaction before accepting"); + let verification_start_timestamp = Timestamp::now(); + let transaction_config = &self.chainspec.as_ref().transaction_config; + let maybe_meta_transaction = MetaTransaction::from(&input_transaction, transaction_config); + let meta_transaction = match maybe_meta_transaction { + Ok(transaction) => transaction, + Err(err) => { + return self.reject_transaction_direct( + effect_builder, + input_transaction, + source, + maybe_responder, + verification_start_timestamp, + Error::InvalidTransaction(err), + ); + } }; + let event_metadata = Box::new(EventMetadata::new( + input_transaction, + meta_transaction, + source, + maybe_responder, + verification_start_timestamp, + )); + let is_config_compliant = event_metadata + .meta_transaction + .is_config_compliant( + &self.chainspec, + self.acceptor_config.timestamp_leeway, + verification_start_timestamp, + ) + .map_err(Error::InvalidTransaction); if let Err(error) = is_config_compliant { return self.reject_transaction(effect_builder, *event_metadata, error); @@ -356,11 +370,11 @@ impl TransactionAcceptor { event_metadata: Box, block_header: Box, ) -> Effects { - match &event_metadata.transaction { - Transaction::Deploy(_) => { + match &event_metadata.meta_transaction { + MetaTransaction::Deploy(_) => { self.verify_deploy_session(effect_builder, event_metadata, block_header) } - Transaction::V1(_) => { + MetaTransaction::V1(_) => { self.verify_transaction_v1_body(effect_builder, event_metadata, block_header) } } @@ -372,9 +386,9 @@ impl TransactionAcceptor { event_metadata: Box, block_header: Box, ) -> Effects { - let session = match &event_metadata.transaction { - Transaction::Deploy(deploy) => deploy.session(), - Transaction::V1(txn) => { + let session = match &event_metadata.meta_transaction { + MetaTransaction::Deploy(deploy) => deploy.session(), + MetaTransaction::V1(txn) => { error!(%txn, "should only handle deploys in verify_deploy_session"); return self.reject_transaction( effect_builder, @@ -478,8 +492,8 @@ impl TransactionAcceptor { CryptoValidation, } - let next_step = match &event_metadata.transaction { - Transaction::Deploy(deploy) => { + let next_step = match &event_metadata.meta_transaction { + MetaTransaction::Deploy(deploy) => { error!( %deploy, "should only handle version 1 transactions in verify_transaction_v1_body" @@ -490,7 +504,7 @@ impl TransactionAcceptor { Error::ExpectedTransactionV1, ); } - Transaction::V1(txn) => match txn.target() { + MetaTransaction::V1(txn) => match txn.target() { TransactionTarget::Stored { id, .. } => match id { TransactionInvocationTarget::ByHash(entity_addr) => { NextStep::GetContract(EntityAddr::SmartContract(*entity_addr)) @@ -559,16 +573,18 @@ impl TransactionAcceptor { return self.reject_transaction(effect_builder, *event_metadata, error); } - let maybe_entry_point_name = match &event_metadata.transaction { - Transaction::Deploy(deploy) if is_payment => { + let maybe_entry_point_name = match &event_metadata.meta_transaction { + MetaTransaction::Deploy(deploy) if is_payment => { Some(deploy.payment().entry_point_name().to_string()) } - Transaction::Deploy(deploy) => Some(deploy.session().entry_point_name().to_string()), - Transaction::V1(_) if is_payment => { + MetaTransaction::Deploy(deploy) => { + Some(deploy.session().entry_point_name().to_string()) + } + MetaTransaction::V1(_) if is_payment => { error!("should not fetch a contract to validate payment logic for transaction v1s"); None } - Transaction::V1(txn) => match txn.entry_point() { + MetaTransaction::V1(txn) => match txn.entry_point() { TransactionEntryPoint::Call => Some(DEFAULT_ENTRY_POINT_NAME.to_owned()), TransactionEntryPoint::Custom(name) => Some(name.clone()), TransactionEntryPoint::Transfer @@ -737,11 +753,11 @@ impl TransactionAcceptor { effect_builder: EffectBuilder, event_metadata: Box, ) -> Effects { - let is_valid = match &event_metadata.transaction { - Transaction::Deploy(deploy) => deploy + let is_valid = match &event_metadata.meta_transaction { + MetaTransaction::Deploy(deploy) => deploy .is_valid() .map_err(|err| Error::InvalidTransaction(err.into())), - Transaction::V1(txn) => txn + MetaTransaction::V1(txn) => txn .verify() .map_err(|err| Error::InvalidTransaction(err.into())), }; @@ -775,11 +791,32 @@ impl TransactionAcceptor { ) -> Effects { debug!(%error, transaction = %event_metadata.transaction, "rejected transaction"); let EventMetadata { + meta_transaction: _, transaction, source, maybe_responder, verification_start_timestamp, } = event_metadata; + self.reject_transaction_direct( + effect_builder, + transaction, + source, + maybe_responder, + verification_start_timestamp, + error, + ) + } + + fn reject_transaction_direct( + &self, + effect_builder: EffectBuilder, + transaction: Transaction, + source: Source, + maybe_responder: Option>>, + verification_start_timestamp: Timestamp, + error: Error, + ) -> Effects { + debug!(%error, transaction = %transaction, "rejected transaction"); self.metrics.observe_rejected(verification_start_timestamp); let mut effects = Effects::new(); if let Some(responder) = maybe_responder { @@ -847,6 +884,7 @@ impl TransactionAcceptor { is_new: bool, ) -> Effects { let EventMetadata { + meta_transaction: _, transaction, source, maybe_responder, diff --git a/node/src/components/transaction_acceptor/event.rs b/node/src/components/transaction_acceptor/event.rs index 459608144f..60c9539e9a 100644 --- a/node/src/components/transaction_acceptor/event.rs +++ b/node/src/components/transaction_acceptor/event.rs @@ -8,12 +8,13 @@ use casper_types::{ }; use super::{Error, Source}; -use crate::effect::Responder; +use crate::{effect::Responder, types::MetaTransaction}; /// A utility struct to hold duplicated information across events. #[derive(Debug, Serialize)] pub(crate) struct EventMetadata { pub(crate) transaction: Transaction, + pub(crate) meta_transaction: MetaTransaction, pub(crate) source: Source, pub(crate) maybe_responder: Option>>, pub(crate) verification_start_timestamp: Timestamp, @@ -22,14 +23,17 @@ pub(crate) struct EventMetadata { impl EventMetadata { pub(crate) fn new( transaction: Transaction, + meta_transaction: MetaTransaction, source: Source, maybe_responder: Option>>, + verification_start_timestamp: Timestamp, ) -> Self { EventMetadata { transaction, + meta_transaction, source, maybe_responder, - verification_start_timestamp: Timestamp::now(), + verification_start_timestamp, } } } diff --git a/node/src/components/transaction_acceptor/tests.rs b/node/src/components/transaction_acceptor/tests.rs index c588c9cdaa..db8434f8b0 100644 --- a/node/src/components/transaction_acceptor/tests.rs +++ b/node/src/components/transaction_acceptor/tests.rs @@ -1,7 +1,7 @@ #![cfg(test)] use std::{ - collections::VecDeque, + collections::{BTreeMap, VecDeque}, fmt::{self, Debug, Display, Formatter}, iter, sync::Arc, @@ -37,8 +37,7 @@ use casper_types::{ Block, BlockV2, CLValue, Chainspec, ChainspecRawBytes, Contract, Deploy, EntryPointValue, EraId, HashAddr, InvalidDeploy, InvalidTransaction, InvalidTransactionV1, Package, PricingMode, ProtocolVersion, PublicKey, SecretKey, StoredValue, TestBlockBuilder, TimeDiff, Timestamp, - Transaction, TransactionCategory, TransactionConfig, TransactionV1, TransactionV1Builder, URef, - U512, + Transaction, TransactionConfig, TransactionV1, TransactionV1Builder, URef, U512, }; use super::*; @@ -214,6 +213,8 @@ enum TestScenario { InvalidPricingModeForTransactionV1, TooLowGasPriceToleranceForTransactionV1, TooLowGasPriceToleranceForDeploy, + InvalidFields, + InvalidFieldsFromPeer, } impl TestScenario { @@ -230,7 +231,8 @@ impl TestScenario { | TestScenario::FromPeerCustomPaymentContract(_) | TestScenario::FromPeerCustomPaymentContractPackage(_) | TestScenario::FromPeerSessionContract(..) - | TestScenario::FromPeerSessionContractPackage(..) => Source::Peer(NodeId::random(rng)), + | TestScenario::FromPeerSessionContractPackage(..) + | TestScenario::InvalidFieldsFromPeer => Source::Peer(NodeId::random(rng)), TestScenario::FromClientInvalidTransaction(_) | TestScenario::FromClientSlightlyFutureDatedTransaction(_) | TestScenario::FromClientFutureDatedTransaction(_) @@ -256,7 +258,8 @@ impl TestScenario { | TestScenario::DeployWithNativeTransferInPayment | TestScenario::InvalidPricingModeForTransactionV1 | TestScenario::TooLowGasPriceToleranceForTransactionV1 - | TestScenario::TooLowGasPriceToleranceForDeploy => Source::Client, + | TestScenario::TooLowGasPriceToleranceForDeploy + | TestScenario::InvalidFields => Source::Client, } } @@ -281,15 +284,12 @@ impl TestScenario { } TestScenario::FromPeerExpired(TxnType::V1) | TestScenario::FromClientExpired(TxnType::V1) => { - let txn = TransactionV1Builder::new_session( - TransactionCategory::Large, - Bytes::from(vec![1]), - ) - .with_chain_name("casper-example") - .with_timestamp(Timestamp::zero()) - .with_secret_key(&secret_key) - .build() - .unwrap(); + let txn = TransactionV1Builder::new_session(false, Bytes::from(vec![1])) + .with_chain_name("casper-example") + .with_timestamp(Timestamp::zero()) + .with_secret_key(&secret_key) + .build() + .unwrap(); Transaction::from(txn) } TestScenario::FromPeerValidTransaction(txn_type) @@ -305,15 +305,12 @@ impl TestScenario { | TestScenario::FromClientAccountWithInsufficientWeight(txn_type) => match txn_type { TxnType::Deploy => Transaction::from(Deploy::random_valid_native_transfer(rng)), TxnType::V1 => { - let txn = TransactionV1Builder::new_session( - TransactionCategory::Large, - Bytes::from(vec![1]), - ) - .with_chain_name("casper-example") - .with_timestamp(Timestamp::now()) - .with_secret_key(&secret_key) - .build() - .unwrap(); + let txn = TransactionV1Builder::new_session(false, Bytes::from(vec![1])) + .with_chain_name("casper-example") + .with_timestamp(Timestamp::now()) + .with_secret_key(&secret_key) + .build() + .unwrap(); Transaction::from(txn) } }, @@ -323,15 +320,12 @@ impl TestScenario { Transaction::from(deploy) } TestScenario::FromClientSignedByAdmin(TxnType::V1) => { - let txn = TransactionV1Builder::new_session( - TransactionCategory::Large, - Bytes::from(vec![1]), - ) - .with_chain_name("casper-example") - .with_timestamp(Timestamp::now()) - .with_secret_key(admin) - .build() - .unwrap(); + let txn = TransactionV1Builder::new_session(false, Bytes::from(vec![1])) + .with_chain_name("casper-example") + .with_timestamp(Timestamp::now()) + .with_secret_key(admin) + .build() + .unwrap(); Transaction::from(txn) } TestScenario::AccountWithUnknownBalance @@ -518,16 +512,13 @@ impl TestScenario { ), ), TxnType::V1 => { - let txn = TransactionV1Builder::new_session( - TransactionCategory::Large, - Bytes::from(vec![1]), - ) - .with_chain_name("casper-example") - .with_timestamp(timestamp) - .with_ttl(ttl) - .with_secret_key(&secret_key) - .build() - .unwrap(); + let txn = TransactionV1Builder::new_session(false, Bytes::from(vec![1])) + .with_chain_name("casper-example") + .with_timestamp(timestamp) + .with_ttl(ttl) + .with_secret_key(&secret_key) + .build() + .unwrap(); Transaction::from(txn) } } @@ -544,16 +535,13 @@ impl TestScenario { ), ), TxnType::V1 => { - let txn = TransactionV1Builder::new_session( - TransactionCategory::Large, - Bytes::from(vec![1]), - ) - .with_chain_name("casper-example") - .with_timestamp(timestamp) - .with_ttl(ttl) - .with_secret_key(&secret_key) - .build() - .unwrap(); + let txn = TransactionV1Builder::new_session(false, Bytes::from(vec![1])) + .with_chain_name("casper-example") + .with_timestamp(timestamp) + .with_ttl(ttl) + .with_secret_key(&secret_key) + .build() + .unwrap(); Transaction::from(txn) } } @@ -576,6 +564,7 @@ impl TestScenario { let fixed_mode_transaction = TransactionV1Builder::new_random(rng) .with_pricing_mode(PricingMode::Fixed { gas_price_tolerance: TOO_LOW_GAS_PRICE_TOLERANCE, + additional_computation_factor: 0, }) .with_chain_name("casper-example") .build() @@ -588,6 +577,18 @@ impl TestScenario { let deploy = Deploy::random_with_gas_price(rng, TOO_LOW_GAS_PRICE_TOLERANCE); Transaction::from(deploy) } + TestScenario::InvalidFields | TestScenario::InvalidFieldsFromPeer => { + let mut additional_fields = BTreeMap::new(); + additional_fields.insert(5, Bytes::from(vec![1])); + let txn = TransactionV1Builder::new_session(false, Bytes::from(vec![1])) + .with_chain_name("casper-example") + .with_ttl(TimeDiff::from_seconds(300)) + .with_secret_key(&secret_key) + .with_additional_fields(additional_fields) + .build() + .unwrap(); + Transaction::from(txn) + } } } @@ -644,6 +645,8 @@ impl TestScenario { TestScenario::InvalidPricingModeForTransactionV1 => false, TestScenario::TooLowGasPriceToleranceForTransactionV1 => false, TestScenario::TooLowGasPriceToleranceForDeploy => false, + TestScenario::InvalidFields => false, + TestScenario::InvalidFieldsFromPeer => false, } } @@ -1058,12 +1061,20 @@ fn inject_balance_check_for_peer( source: Source, rng: &mut TestRng, responder: Responder>, + chainspec: &Chainspec, ) -> impl FnOnce(EffectBuilder) -> Effects { let txn = txn.clone(); let block = TestBlockBuilder::new().build(rng); let block_header = Box::new(block.header().clone().into()); + let meta_transaction = MetaTransaction::from(&txn, &chainspec.transaction_config).unwrap(); |effect_builder: EffectBuilder| { - let event_metadata = Box::new(EventMetadata::new(txn, source, Some(responder))); + let event_metadata = Box::new(EventMetadata::new( + txn, + meta_transaction, + source, + Some(responder), + Timestamp::now(), + )); effect_builder .into_inner() .schedule( @@ -1089,9 +1100,10 @@ async fn run_transaction_acceptor_without_timeout( <(Chainspec, ChainspecRawBytes)>::from_resources("local"); chainspec.core_config.administrators = iter::once(PublicKey::from(&admin)).collect(); + let chainspec = Arc::new(chainspec); let mut runner: Runner> = Runner::new( test_scenario, - Arc::new(chainspec), + chainspec.clone(), Arc::new(chainspec_raw_bytes), rng, ) @@ -1141,12 +1153,14 @@ async fn run_transaction_acceptor_without_timeout( if test_scenario == TestScenario::BalanceCheckForDeploySentByPeer { let (txn_sender, _) = oneshot::channel(); let txn_responder = Responder::without_shutdown(txn_sender); + let chainspec = chainspec.as_ref().clone(); runner .process_injected_effects(inject_balance_check_for_peer( &txn, source.clone(), rng, txn_responder, + &chainspec, )) .await; while runner.try_crank(rng).await == TryCrankOutcome::NoEventsToProcess { @@ -1182,7 +1196,8 @@ async fn run_transaction_acceptor_without_timeout( | TestScenario::InvalidPricingModeForTransactionV1 | TestScenario::FromClientExpired(_) | TestScenario::TooLowGasPriceToleranceForTransactionV1 - | TestScenario::TooLowGasPriceToleranceForDeploy => { + | TestScenario::TooLowGasPriceToleranceForDeploy + | TestScenario::InvalidFields => { matches!( event, Event::TransactionAcceptorAnnouncement( @@ -1244,7 +1259,8 @@ async fn run_transaction_acceptor_without_timeout( // Check that invalid transactions sent by a peer raise the `InvalidTransaction` // announcement with the appropriate source. TestScenario::FromPeerInvalidTransaction(_) - | TestScenario::BalanceCheckForDeploySentByPeer => { + | TestScenario::BalanceCheckForDeploySentByPeer + | TestScenario::InvalidFieldsFromPeer => { matches!( event, Event::TransactionAcceptorAnnouncement( @@ -2430,3 +2446,25 @@ async fn should_reject_deploy_with_too_low_gas_price_tolerance() { )) )) } + +#[tokio::test] +async fn should_reject_transaction_with_unexpected_fields() { + let result = run_transaction_acceptor(TestScenario::InvalidFields).await; + assert!(matches!( + result, + Err(super::Error::InvalidTransaction(InvalidTransaction::V1( + InvalidTransactionV1::UnexpectedTransactionFieldEntries + ))) + )) +} + +#[tokio::test] +async fn should_reject_transaction_from_peer_with_unexpected_fields() { + let result = run_transaction_acceptor(TestScenario::InvalidFieldsFromPeer).await; + assert!(matches!( + result, + Err(super::Error::InvalidTransaction(InvalidTransaction::V1( + InvalidTransactionV1::UnexpectedTransactionFieldEntries + ))) + )) +} diff --git a/node/src/components/transaction_buffer.rs b/node/src/components/transaction_buffer.rs index a619519106..8cf1c63a78 100644 --- a/node/src/components/transaction_buffer.rs +++ b/node/src/components/transaction_buffer.rs @@ -260,6 +260,7 @@ impl TransactionBuffer { error!(%transaction_hash, "TransactionBuffer: invalid transaction must not be buffered"); return; } + if self .hold .values() @@ -268,6 +269,7 @@ impl TransactionBuffer { info!(%transaction_hash, "TransactionBuffer: attempt to register already held transaction"); return; } + let footprint = match TransactionFootprint::new(&self.chainspec, &transaction) { Ok(footprint) => footprint, Err(invalid_transaction_error) => { @@ -410,7 +412,7 @@ impl TransactionBuffer { let mut buckets: HashMap<_, Vec<_>> = HashMap::new(); for (transaction_hash, footprint) in proposable { buckets - .entry(&footprint.body_hash) + .entry(&footprint.payload_hash) .and_modify(|vec| vec.push((*transaction_hash, footprint))) .or_insert(vec![(*transaction_hash, footprint)]); } @@ -459,9 +461,9 @@ impl TransactionBuffer { let iter_limit = self.buffer.len() * 4; let mut buckets = self.buckets(current_era_gas_price); - let mut body_hashes_queue: VecDeque<_> = buckets.keys().cloned().collect(); + let mut payload_hashes_queue: VecDeque<_> = buckets.keys().cloned().collect(); - while let Some(body_hash) = body_hashes_queue.pop_front() { + while let Some(payload_hash) = payload_hashes_queue.pop_front() { if Timestamp::now() > request_expiry { break; } @@ -475,14 +477,14 @@ impl TransactionBuffer { } let Some((transaction_hash, footprint)) = - buckets.get_mut(body_hash).and_then(Vec::<_>::pop) + buckets.get_mut(payload_hash).and_then(Vec::<_>::pop) else { continue; }; // bucket wasn't empty - push the hash back into the queue to be processed again on the // next pass - body_hashes_queue.push_back(body_hash); + payload_hashes_queue.push_back(payload_hash); if footprint.is_mint() && have_hit_mint_limit { continue; @@ -522,15 +524,15 @@ impl TransactionBuffer { ); dead.insert(transaction_hash); } - AddError::Count(category) => { - match category { - category if category == MINT_LANE_ID => { + AddError::Count(lane_id) => { + match lane_id { + lane_id if lane_id == MINT_LANE_ID => { have_hit_mint_limit = true; } - category if category == AUCTION_LANE_ID => { + lane_id if lane_id == AUCTION_LANE_ID => { have_hit_auction_limit = true; } - category if category == INSTALL_UPGRADE_LANE_ID => { + lane_id if lane_id == INSTALL_UPGRADE_LANE_ID => { have_hit_install_upgrade_limit = true; } _ => { diff --git a/node/src/components/transaction_buffer/tests.rs b/node/src/components/transaction_buffer/tests.rs index 4119141304..9c8ef7d31b 100644 --- a/node/src/components/transaction_buffer/tests.rs +++ b/node/src/components/transaction_buffer/tests.rs @@ -5,7 +5,8 @@ use rand::{seq::SliceRandom, Rng}; use casper_types::{ testing::TestRng, Deploy, EraId, SecretKey, TestBlockBuilder, TimeDiff, Transaction, - TransactionConfig, TransactionV1, TransactionV1Config, DEFAULT_LARGE_TRANSACTION_GAS_LIMIT, + TransactionConfig, TransactionLimitsDefinition, TransactionV1, TransactionV1Config, + DEFAULT_LARGE_TRANSACTION_GAS_LIMIT, LARGE_WASM_LANE_ID, }; use super::*; @@ -19,7 +20,6 @@ use crate::{ const ERA_ONE: EraId = EraId::new(1u64); const GAS_PRICE_TOLERANCE: u8 = 1; const DEFAULT_MINIMUM_GAS_PRICE: u8 = 1; -const LARGE_LANE_ID: u8 = 3; fn get_appendable_block( rng: &mut TestRng, @@ -53,7 +53,7 @@ fn get_appendable_block( // Generates valid transactions fn create_valid_transaction( rng: &mut TestRng, - transaction_category: u8, + transaction_lane: u8, strict_timestamp: Option, with_ttl: Option, ) -> Transaction { @@ -66,8 +66,8 @@ fn create_valid_transaction( None => Timestamp::now(), }; - match transaction_category { - transaction_category if transaction_category == MINT_LANE_ID => { + match transaction_lane { + transaction_lane if transaction_lane == MINT_LANE_ID => { if rng.gen() { Transaction::V1(TransactionV1::random_transfer( rng, @@ -82,10 +82,10 @@ fn create_valid_transaction( )) } } - transaction_category if transaction_category == INSTALL_UPGRADE_LANE_ID => Transaction::V1( + transaction_lane if transaction_lane == INSTALL_UPGRADE_LANE_ID => Transaction::V1( TransactionV1::random_install_upgrade(rng, strict_timestamp, with_ttl), ), - transaction_category if transaction_category == AUCTION_LANE_ID => Transaction::V1( + transaction_lane if transaction_lane == AUCTION_LANE_ID => Transaction::V1( TransactionV1::random_auction(rng, strict_timestamp, with_ttl), ), _ => { @@ -163,17 +163,17 @@ const fn all_categories() -> [u8; 4] { MINT_LANE_ID, INSTALL_UPGRADE_LANE_ID, AUCTION_LANE_ID, - LARGE_LANE_ID, + LARGE_WASM_LANE_ID, ] } #[test] fn register_transaction_and_check_size() { let mut rng = TestRng::new(); - + let chainspec = Chainspec::default(); for category in all_categories() { let mut transaction_buffer = TransactionBuffer::new( - Arc::new(Chainspec::default()), + Arc::new(chainspec.clone()), Config::default(), &Registry::new(), ) @@ -194,7 +194,7 @@ fn register_transaction_and_check_size() { .get(rng.gen_range(0..num_valid_transactions)) .unwrap() .clone(); - transaction_buffer.register_transaction(duplicate_transaction); + transaction_buffer.register_transaction(duplicate_transaction.clone()); assert_container_sizes(&transaction_buffer, valid_transactions.len(), 0, 0); // Insert transaction without footprint @@ -558,8 +558,9 @@ fn block_fully_saturated() { // Ensure that only 'total_allowed' transactions are proposed. let timestamp = Timestamp::now(); - let expiry = timestamp.saturating_add(TimeDiff::from_seconds(1)); + let expiry = timestamp.saturating_add(TimeDiff::from_seconds(60)); let appendable_block = transaction_buffer.appendable_block(Timestamp::now(), ERA_ONE, expiry); + assert_eq!( appendable_block.transaction_hashes().len(), total_allowed as usize @@ -584,7 +585,6 @@ fn block_fully_saturated() { proposed_standards += 1; } }); - let mut has_hit_any_limit = false; if proposed_transfers == max_transfers { has_hit_any_limit = true; @@ -592,7 +592,7 @@ fn block_fully_saturated() { if proposed_stakings == max_staking { has_hit_any_limit = true; } - if proposed_install_upgrades as u64 == max_install_upgrade { + if proposed_install_upgrades == max_install_upgrade { has_hit_any_limit = true; } if proposed_standards == max_standard { @@ -836,7 +836,8 @@ fn register_transactions_and_blocks() { .cloned() .peekable(); assert!(held_transactions.peek().is_some()); - held_transactions.for_each(|transaction| transaction_buffer.register_transaction(transaction)); + held_transactions + .for_each(|transaction| transaction_buffer.register_transaction(transaction.clone())); assert_container_sizes( &transaction_buffer, block_transaction.len() + valid_transactions.len(), @@ -1090,11 +1091,13 @@ fn make_test_chainspec(max_standard_count: u64, max_mint_count: u64) -> Arc { @@ -2964,6 +2964,7 @@ async fn run_gas_price_scenario(gas_price_scenario: GasPriceScenario) { .with_ttl(TimeDiff::from_seconds(120 * 10)) .with_pricing_mode(PricingMode::Fixed { gas_price_tolerance: max_gas_price, + additional_computation_factor: 0, }) .build() .expect("must get transaction"); @@ -2996,6 +2997,7 @@ async fn run_gas_price_scenario(gas_price_scenario: GasPriceScenario) { .with_secret_key(&alice_secret_key) .with_pricing_mode(PricingMode::Fixed { gas_price_tolerance: max_gas_price, + additional_computation_factor: 0, }) .build() .expect("must get transaction"); diff --git a/node/src/reactor/main_reactor/tests/transactions.rs b/node/src/reactor/main_reactor/tests/transactions.rs index 09baf1f6ac..cd4d84f81f 100644 --- a/node/src/reactor/main_reactor/tests/transactions.rs +++ b/node/src/reactor/main_reactor/tests/transactions.rs @@ -1,4 +1,5 @@ use super::*; +use crate::types::MetaTransaction; use casper_execution_engine::engine_state::MAX_PAYMENT_AMOUNT; use casper_storage::data_access_layer::{ AddressableEntityRequest, BalanceIdentifier, ProofHandling, QueryRequest, QueryResult, @@ -8,7 +9,7 @@ use casper_types::{ addressable_entity::NamedKeyAddr, runtime_args, system::mint::{ARG_AMOUNT, ARG_TARGET}, - AddressableEntity, Digest, EntityAddr, ExecutionInfo, GasLimited, TransactionCategory, + AddressableEntity, Digest, EntityAddr, ExecutionInfo, LARGE_WASM_LANE_ID, }; use once_cell::sync::Lazy; @@ -33,7 +34,6 @@ static CHARLIE_PUBLIC_KEY: Lazy = const MIN_GAS_PRICE: u8 = 5; const CHAIN_NAME: &str = "single-transaction-test-net"; -const LARGE_LANE_ID: u8 = 3; async fn transfer_to_account>( fixture: &mut TestFixture, @@ -89,8 +89,13 @@ async fn send_wasm_transaction( ) -> (TransactionHash, u64, ExecutionResult) { let chain_name = fixture.chainspec.network_config.name.clone(); + //These bytes are intentionally so large - this way they fall into "WASM_LARGE" category in the + // local chainspec Alternatively we could change the chainspec to have a different limits + // for the wasm categories, but that would require aligning all tests that use local + // chainspec + let module_bytes = Bytes::from(vec![1; 172_033]); let mut txn = Transaction::from( - TransactionV1Builder::new_session(TransactionCategory::Large, Bytes::from(vec![1])) + TransactionV1Builder::new_session(false, module_bytes) .with_chain_name(chain_name) .with_pricing_mode(pricing) .with_initiator_addr(PublicKey::from(from)) @@ -391,6 +396,7 @@ async fn transfer_cost_fixed_price_no_fee_no_refund() { PublicKey::from(&*charlie_secret_key), PricingMode::Fixed { gas_price_tolerance: 1, + additional_computation_factor: 0, }, Some(0xDEADBEEF), ) @@ -496,6 +502,7 @@ async fn should_accept_transfer_without_id() { PublicKey::from(&*charlie_secret_key), PricingMode::Fixed { gas_price_tolerance: 1, + additional_computation_factor: 0, }, None, ) @@ -536,6 +543,7 @@ async fn failed_transfer_cost_fixed_price_no_fee_no_refund() { PublicKey::from(&*charlie_secret_key), PricingMode::Fixed { gas_price_tolerance: 1, + additional_computation_factor: 0, }, None, ) @@ -551,6 +559,7 @@ async fn failed_transfer_cost_fixed_price_no_fee_no_refund() { PublicKey::from(&*bob_secret_key), PricingMode::Fixed { gas_price_tolerance: 1, + additional_computation_factor: 0, }, None, ) @@ -772,6 +781,7 @@ async fn native_operations_fees_are_not_refunded() { PublicKey::from(&*charlie_secret_key), PricingMode::Fixed { gas_price_tolerance: MIN_GAS_PRICE, + additional_computation_factor: 0, }, None, ) @@ -869,6 +879,7 @@ async fn wasm_transaction_fees_are_refunded() { &bob_secret_key, PricingMode::Fixed { gas_price_tolerance: MIN_GAS_PRICE, + additional_computation_factor: 0, }, ) .await; @@ -877,7 +888,7 @@ async fn wasm_transaction_fees_are_refunded() { let expected_transaction_gas: u64 = fixture .chainspec - .get_max_gas_limit_by_category(LARGE_LANE_ID); + .get_max_gas_limit_by_category(LARGE_WASM_LANE_ID); let expected_transaction_cost = expected_transaction_gas * MIN_GAS_PRICE as u64; assert_exec_result_cost( exec_result, @@ -1192,7 +1203,7 @@ async fn wasm_transaction_refunds_are_burnt(txn_pricing_mode: PricingMode) { let expected_transaction_gas: u64 = gas_limit.unwrap_or( test.chainspec() - .get_max_gas_limit_by_category(LARGE_LANE_ID), + .get_max_gas_limit_by_category(LARGE_WASM_LANE_ID), ); let expected_transaction_cost = expected_transaction_gas * min_gas_price as u64; @@ -1249,6 +1260,7 @@ async fn wasm_transaction_refunds_are_burnt(txn_pricing_mode: PricingMode) { async fn wasm_transaction_refunds_are_burnt_fixed_pricing() { wasm_transaction_refunds_are_burnt(PricingMode::Fixed { gas_price_tolerance: MIN_GAS_PRICE, + additional_computation_factor: 0, }) .await; } @@ -1293,7 +1305,7 @@ async fn only_refunds_are_burnt_no_fee(txn_pricing_mode: PricingMode) { // Fixed transaction pricing. let expected_transaction_gas: u64 = gas_limit.unwrap_or( test.chainspec() - .get_max_gas_limit_by_category(LARGE_LANE_ID), + .get_max_gas_limit_by_category(LARGE_WASM_LANE_ID), ); let expected_transaction_cost = expected_transaction_gas * min_gas_price as u64; @@ -1350,6 +1362,7 @@ async fn only_refunds_are_burnt_no_fee(txn_pricing_mode: PricingMode) { async fn only_refunds_are_burnt_no_fee_fixed_pricing() { only_refunds_are_burnt_no_fee(PricingMode::Fixed { gas_price_tolerance: MIN_GAS_PRICE, + additional_computation_factor: 0, }) .await; } @@ -1385,7 +1398,7 @@ async fn fees_and_refunds_are_burnt_separately(txn_pricing_mode: PricingMode) { // Fixed transaction pricing. let expected_transaction_gas: u64 = gas_limit.unwrap_or( test.chainspec() - .get_max_gas_limit_by_category(LARGE_LANE_ID), + .get_max_gas_limit_by_category(LARGE_WASM_LANE_ID), ); let expected_transaction_cost = expected_transaction_gas * min_gas_price as u64; @@ -1444,6 +1457,7 @@ async fn fees_and_refunds_are_burnt_separately(txn_pricing_mode: PricingMode) { async fn fees_and_refunds_are_burnt_separately_fixed_pricing() { fees_and_refunds_are_burnt_separately(PricingMode::Fixed { gas_price_tolerance: MIN_GAS_PRICE, + additional_computation_factor: 0, }) .await; } @@ -1480,7 +1494,7 @@ async fn refunds_are_payed_and_fees_are_burnt(txn_pricing_mode: PricingMode) { // Fixed transaction pricing. let expected_transaction_gas: u64 = gas_limit.unwrap_or( test.chainspec() - .get_max_gas_limit_by_category(LARGE_LANE_ID), + .get_max_gas_limit_by_category(LARGE_WASM_LANE_ID), ); let expected_transaction_cost = expected_transaction_gas * min_gas_price as u64; @@ -1545,6 +1559,7 @@ async fn refunds_are_payed_and_fees_are_burnt(txn_pricing_mode: PricingMode) { async fn refunds_are_payed_and_fees_are_burnt_fixed_pricing() { refunds_are_payed_and_fees_are_burnt(PricingMode::Fixed { gas_price_tolerance: MIN_GAS_PRICE, + additional_computation_factor: 0, }) .await; } @@ -1577,10 +1592,15 @@ async fn refunds_are_payed_and_fees_are_on_hold(txn_pricing_mode: PricingMode) { .await; let txn = invalid_wasm_txn(BOB_SECRET_KEY.clone(), txn_pricing_mode); + let meta_transaction = + MetaTransaction::from(&txn, &test.chainspec().transaction_config).unwrap(); // Fixed transaction pricing. let expected_consumed_gas = Gas::new(0); // expect that this transaction doesn't consume any gas since it has invalid wasm. - let expected_transaction_cost = - txn.gas_limit(test.chainspec()).unwrap().value() * min_gas_price; + let expected_transaction_cost = meta_transaction + .gas_limit(test.chainspec()) + .unwrap() + .value() + * min_gas_price; test.fixture .run_until_consensus_in_era(ERA_ONE, ONE_MIN) @@ -1645,6 +1665,7 @@ async fn refunds_are_payed_and_fees_are_on_hold(txn_pricing_mode: PricingMode) { async fn refunds_are_payed_and_fees_are_on_hold_fixed_pricing() { refunds_are_payed_and_fees_are_on_hold(PricingMode::Fixed { gas_price_tolerance: MIN_GAS_PRICE, + additional_computation_factor: 0, }) .await; } @@ -1688,7 +1709,7 @@ async fn only_refunds_are_burnt_no_fee_custom_payment() { let expected_transaction_cost = expected_transaction_gas * MIN_GAS_PRICE as u64; let mut txn = Transaction::from( - TransactionV1Builder::new_session(TransactionCategory::Large, module_bytes) + TransactionV1Builder::new_session(false, module_bytes) .with_chain_name(CHAIN_NAME) .with_pricing_mode(PricingMode::Classic { payment_amount: expected_transaction_gas, @@ -1786,7 +1807,7 @@ async fn no_refund_no_fee_custom_payment() { let expected_transaction_cost = expected_transaction_gas * MIN_GAS_PRICE as u64; let mut txn = Transaction::from( - TransactionV1Builder::new_session(TransactionCategory::Large, module_bytes) + TransactionV1Builder::new_session(false, module_bytes) .with_chain_name(CHAIN_NAME) .with_pricing_mode(PricingMode::Classic { payment_amount: expected_transaction_gas, @@ -1954,6 +1975,7 @@ async fn transfer_fee_is_burnt_no_refund(txn_pricing_mode: PricingMode) { async fn transfer_fee_is_burnt_no_refund_fixed_pricing() { transfer_fee_is_burnt_no_refund(PricingMode::Fixed { gas_price_tolerance: MIN_GAS_PRICE, + additional_computation_factor: 0, }) .await; } @@ -2064,6 +2086,7 @@ async fn fee_is_payed_to_proposer_no_refund(txn_pricing_mode: PricingMode) { async fn fee_is_payed_to_proposer_no_refund_fixed_pricing() { fee_is_payed_to_proposer_no_refund(PricingMode::Fixed { gas_price_tolerance: MIN_GAS_PRICE, + additional_computation_factor: 0, }) .await; } @@ -2109,7 +2132,7 @@ async fn wasm_transaction_fees_are_refunded_to_proposer(txn_pricing_mode: Pricin let expected_transaction_gas: u64 = gas_limit.unwrap_or( test.chainspec() - .get_max_gas_limit_by_category(LARGE_LANE_ID), + .get_max_gas_limit_by_category(LARGE_WASM_LANE_ID), ); let expected_transaction_cost = expected_transaction_gas * min_gas_price as u64; @@ -2165,6 +2188,7 @@ async fn wasm_transaction_fees_are_refunded_to_proposer(txn_pricing_mode: Pricin async fn wasm_transaction_fees_are_refunded_to_proposer_fixed_pricing() { wasm_transaction_fees_are_refunded_to_proposer(PricingMode::Fixed { gas_price_tolerance: MIN_GAS_PRICE, + additional_computation_factor: 0, }) .await; } @@ -2303,6 +2327,7 @@ async fn fee_is_accumulated_and_distributed_no_refund(txn_pricing_mode: PricingM async fn fee_is_accumulated_and_distributed_no_refund_fixed_pricing() { fee_is_accumulated_and_distributed_no_refund(PricingMode::Fixed { gas_price_tolerance: MIN_GAS_PRICE, + additional_computation_factor: 0, }) .await; } @@ -2337,8 +2362,13 @@ fn transfer_txn>( } fn invalid_wasm_txn(initiator: Arc, pricing_mode: PricingMode) -> Transaction { + //These bytes are intentionally so large - this way they fall into "WASM_LARGE" category in the + // local chainspec Alternatively we could change the chainspec to have a different limits + // for the wasm categories, but that would require aligning all tests that use local + // chainspec + let module_bytes = Bytes::from(vec![1; 172_033]); let mut txn = Transaction::from( - TransactionV1Builder::new_session(TransactionCategory::Large, Bytes::from(vec![1])) + TransactionV1Builder::new_session(false, module_bytes) .with_chain_name(CHAIN_NAME) .with_pricing_mode(pricing_mode) .with_initiator_addr(PublicKey::from(&*initiator)) @@ -2362,6 +2392,7 @@ fn match_pricing_mode(txn_pricing_mode: &PricingMode) -> (PricingHandling, u8, O ), PricingMode::Fixed { gas_price_tolerance, + .. } => (PricingHandling::Fixed, *gas_price_tolerance, None), PricingMode::Reserved { .. } => unimplemented!(), } @@ -2371,6 +2402,7 @@ fn match_pricing_mode(txn_pricing_mode: &PricingMode) -> (PricingHandling, u8, O async fn holds_should_be_added_and_cleared_fixed_pricing() { holds_should_be_added_and_cleared(PricingMode::Fixed { gas_price_tolerance: MIN_GAS_PRICE, + additional_computation_factor: 0, }) .await; } @@ -2500,6 +2532,7 @@ async fn fee_holds_are_amortized() { BOB_SECRET_KEY.clone(), PricingMode::Fixed { gas_price_tolerance: MIN_GAS_PRICE, + additional_computation_factor: 0, }, ); @@ -2515,7 +2548,7 @@ async fn fee_holds_are_amortized() { // Fixed transaction pricing. let expected_transaction_gas: u64 = test .chainspec() - .get_max_gas_limit_by_category(LARGE_LANE_ID); + .get_max_gas_limit_by_category(LARGE_WASM_LANE_ID); let expected_transaction_cost = expected_transaction_gas * MIN_GAS_PRICE as u64; @@ -2633,6 +2666,7 @@ async fn sufficient_balance_is_available_after_amortization() { &CHARLIE_PUBLIC_KEY, PricingMode::Fixed { gas_price_tolerance: MIN_GAS_PRICE, + additional_computation_factor: 0, }, transfer_amount, ); @@ -2664,6 +2698,7 @@ async fn sufficient_balance_is_available_after_amortization() { &BOB_PUBLIC_KEY, PricingMode::Fixed { gas_price_tolerance: MIN_GAS_PRICE, + additional_computation_factor: 0, }, min_transfer_amount, ); @@ -2704,6 +2739,7 @@ async fn sufficient_balance_is_available_after_amortization() { &BOB_PUBLIC_KEY, PricingMode::Fixed { gas_price_tolerance: MIN_GAS_PRICE, + additional_computation_factor: 0, }, min_transfer_amount, ); @@ -2749,6 +2785,7 @@ async fn validator_credit_is_written_and_cleared_after_auction() { &CHARLIE_PUBLIC_KEY, PricingMode::Fixed { gas_price_tolerance: MIN_GAS_PRICE, + additional_computation_factor: 0, }, transfer_amount, ); @@ -3046,7 +3083,7 @@ async fn insufficient_funds_transfer_from_purse() { Bytes::from(std::fs::read(purse_create_contract).expect("cannot read module bytes")); let mut txn = Transaction::from( - TransactionV1Builder::new_session(TransactionCategory::Large, module_bytes) + TransactionV1Builder::new_session(false, module_bytes) .with_runtime_args( runtime_args! { "destination" => purse_name, "amount" => U512::zero() }, ) @@ -3171,13 +3208,19 @@ async fn charge_when_session_code_succeeds() { let transferred_amount = 1; let mut txn = Transaction::from( - TransactionV1Builder::new_session(TransactionCategory::Large, module_bytes) + TransactionV1Builder::new_session(false, module_bytes) .with_runtime_args(runtime_args! { ARG_TARGET => CHARLIE_PUBLIC_KEY.to_account_hash(), ARG_AMOUNT => U512::from(transferred_amount) }) .with_chain_name(CHAIN_NAME) .with_initiator_addr(BOB_PUBLIC_KEY.clone()) + .with_pricing_mode(PricingMode::Fixed { + gas_price_tolerance: 5, + additional_computation_factor: 2, /*Makes the transaction + * "Large" despite the fact that the actual + * WASM bytes categorize it as "Small" */ + }) .build() .unwrap(), ); @@ -3232,7 +3275,7 @@ async fn charge_when_session_code_fails_with_user_error() { let (alice_initial_balance, bob_initial_balance, _) = test.get_balances(None); let mut txn = Transaction::from( - TransactionV1Builder::new_session(TransactionCategory::Large, module_bytes) + TransactionV1Builder::new_session(false, module_bytes) .with_chain_name(CHAIN_NAME) .with_initiator_addr(BOB_PUBLIC_KEY.clone()) .build() @@ -3296,7 +3339,7 @@ async fn charge_when_session_code_runs_out_of_gas() { let (alice_initial_balance, bob_initial_balance, _) = test.get_balances(None); let mut txn = Transaction::from( - TransactionV1Builder::new_session(TransactionCategory::Large, module_bytes) + TransactionV1Builder::new_session(false, module_bytes) .with_chain_name(CHAIN_NAME) .with_initiator_addr(BOB_PUBLIC_KEY.clone()) .build() @@ -3364,7 +3407,7 @@ async fn successful_purse_to_purse_transfer() { Bytes::from(std::fs::read(purse_create_contract).expect("cannot read module bytes")); let mut txn = Transaction::from( - TransactionV1Builder::new_session(TransactionCategory::Large, module_bytes) + TransactionV1Builder::new_session(false, module_bytes) .with_runtime_args( runtime_args! { "destination" => purse_name, "amount" => U512::from(MAX_PAYMENT_AMOUNT) + U512::one() }, ) @@ -3457,7 +3500,7 @@ async fn successful_purse_to_account_transfer() { Bytes::from(std::fs::read(purse_create_contract).expect("cannot read module bytes")); let mut txn = Transaction::from( - TransactionV1Builder::new_session(TransactionCategory::Large, module_bytes) + TransactionV1Builder::new_session(false, module_bytes) .with_runtime_args( runtime_args! { "destination" => purse_name, "amount" => U512::from(MAX_PAYMENT_AMOUNT) + U512::one() }, ) @@ -3618,7 +3661,7 @@ async fn out_of_gas_txn_does_not_produce_effects() { Bytes::from(std::fs::read(revert_contract).expect("cannot read module bytes")); let mut txn = Transaction::from( - TransactionV1Builder::new_session(TransactionCategory::Large, module_bytes) + TransactionV1Builder::new_session(false, module_bytes) .with_chain_name(CHAIN_NAME) .with_initiator_addr(BOB_PUBLIC_KEY.clone()) .build() @@ -3678,6 +3721,7 @@ async fn gas_holds_accumulate_for_multiple_transactions_in_the_same_block() { let chain_name = test.fixture.chainspec.network_config.name.clone(); let txn_pricing_mode = PricingMode::Fixed { gas_price_tolerance: MIN_GAS_PRICE, + additional_computation_factor: 0, }; let expected_transfer_gas = test.chainspec().system_costs_config.mint_costs().transfer; let expected_transfer_cost: U512 = U512::from(expected_transfer_gas) * MIN_GAS_PRICE; diff --git a/node/src/testing/fake_transaction_acceptor.rs b/node/src/testing/fake_transaction_acceptor.rs index 3e8e21eee7..e275477984 100644 --- a/node/src/testing/fake_transaction_acceptor.rs +++ b/node/src/testing/fake_transaction_acceptor.rs @@ -11,7 +11,7 @@ use std::sync::Arc; use tracing::debug; -use casper_types::Transaction; +use casper_types::{Chainspec, Timestamp, Transaction}; pub(crate) use crate::components::transaction_acceptor::{Error, Event}; use crate::{ @@ -20,6 +20,7 @@ use crate::{ announcements::TransactionAcceptorAnnouncement, requests::StorageRequest, EffectBuilder, EffectExt, Effects, Responder, }, + types::MetaTransaction, utils::Source, NodeRng, }; @@ -39,11 +40,15 @@ impl ReactorEventT for REv where #[derive(Debug)] pub struct FakeTransactionAcceptor { is_active: bool, + chainspec: Chainspec, } impl FakeTransactionAcceptor { pub(crate) fn new() -> Self { - FakeTransactionAcceptor { is_active: true } + FakeTransactionAcceptor { + is_active: true, + chainspec: Chainspec::default(), + } } pub(crate) fn set_active(&mut self, new_setting: bool) { @@ -57,10 +62,14 @@ impl FakeTransactionAcceptor { source: Source, maybe_responder: Option>>, ) -> Effects { + let meta_transaction = + MetaTransaction::from(&transaction, &self.chainspec.transaction_config).unwrap(); let event_metadata = Box::new(EventMetadata::new( transaction.clone(), + meta_transaction, source, maybe_responder, + Timestamp::now(), )); effect_builder .put_transaction_to_storage(transaction) @@ -77,6 +86,7 @@ impl FakeTransactionAcceptor { is_new: bool, ) -> Effects { let EventMetadata { + meta_transaction: _, transaction, source, maybe_responder, diff --git a/node/src/types.rs b/node/src/types.rs index 95a836b2c0..af88a7076a 100644 --- a/node/src/types.rs +++ b/node/src/types.rs @@ -36,7 +36,9 @@ pub use node_config::{NodeConfig, SyncHandling}; pub(crate) use node_id::NodeId; pub use status_feed::{ChainspecInfo, GetStatusResult, StatusFeed}; pub(crate) use sync_leap::{GlobalStatesMetadata, SyncLeap, SyncLeapIdentifier}; -pub(crate) use transaction::{LegacyDeploy, TransactionFootprint}; +pub(crate) use transaction::{ + LegacyDeploy, MetaTransaction, TransactionFootprint, TransactionHeader, +}; pub(crate) use validator_matrix::{EraValidatorWeights, SignatureWeight, ValidatorMatrix}; pub use value_or_chunk::{ ChunkingError, TrieOrChunk, TrieOrChunkId, TrieOrChunkIdDisplay, ValueOrChunk, diff --git a/node/src/types/appendable_block.rs b/node/src/types/appendable_block.rs index 0d98e98b9d..e8857f7bda 100644 --- a/node/src/types/appendable_block.rs +++ b/node/src/types/appendable_block.rs @@ -83,19 +83,19 @@ impl AppendableBlock { if expires < self.timestamp { return Err(AddError::Expired); } - let category = footprint.category; + let lane_id = footprint.lane_id; let limit = self .transaction_config .transaction_v1_config - .get_max_transaction_count(category); + .get_max_transaction_count(lane_id); // check total count by category let count = self .transactions .iter() - .filter(|(_, item)| item.category == category) + .filter(|(_, item)| item.lane_id == lane_id) .count(); - if count.checked_add(1).ok_or(AddError::Count(category))? > limit as usize { - return Err(AddError::Count(category)); + if count.checked_add(1).ok_or(AddError::Count(lane_id))? > limit as usize { + return Err(AddError::Count(lane_id)); } // check total gas let gas_limit: U512 = self @@ -161,7 +161,7 @@ impl AppendableBlock { items: &BTreeMap, ) { let mut ret = vec![]; - for (x, y) in items.iter().filter(|(_, y)| y.category == category) { + for (x, y) in items.iter().filter(|(_, y)| y.lane_id == category) { ret.push((*x, y.approvals.clone())); } if !ret.is_empty() { @@ -177,9 +177,9 @@ impl AppendableBlock { .transaction_v1_config .wasm_lanes .iter() - .map(|lane| lane[0]) + .map(|lane| lane.id()) { - collate(lane_id as u8, &mut transactions, &footprints); + collate(lane_id, &mut transactions, &footprints); } BlockPayload::new( @@ -198,7 +198,7 @@ impl AppendableBlock { fn category_count(&self, category: u8) -> usize { self.transactions .iter() - .filter(|(_, f)| f.category == category) + .filter(|(_, f)| f.lane_id == category) .count() } diff --git a/node/src/types/block/meta_block.rs b/node/src/types/block/meta_block.rs index 93b38957bd..e555d1266e 100644 --- a/node/src/types/block/meta_block.rs +++ b/node/src/types/block/meta_block.rs @@ -3,12 +3,12 @@ mod state; use std::{convert::TryFrom, sync::Arc}; +use crate::types::TransactionHeader; use datasize::DataSize; use serde::Serialize; use casper_types::{ execution::ExecutionResult, ActivationPoint, Block, BlockHash, BlockV2, EraId, TransactionHash, - TransactionHeader, }; pub(crate) use merge_mismatch_error::MergeMismatchError; @@ -185,7 +185,7 @@ mod tests { let txn = TransactionV1::random(rng); let execution_results = vec![ExecutionArtifact::new( TransactionHash::V1(*txn.hash()), - TransactionHeader::V1(txn.take_header()), + (&txn).into(), ExecutionResult::from(ExecutionResultV2::random(rng)), Vec::new(), )]; @@ -240,7 +240,7 @@ mod tests { let txn = TransactionV1::random(rng); let execution_results = vec![ExecutionArtifact::new( TransactionHash::V1(*txn.hash()), - TransactionHeader::V1(txn.take_header()), + (&txn).into(), ExecutionResult::from(ExecutionResultV2::random(rng)), Vec::new(), )]; @@ -278,7 +278,7 @@ mod tests { let txn = TransactionV1::random(rng); let execution_results = vec![ExecutionArtifact::new( TransactionHash::V1(*txn.hash()), - TransactionHeader::V1(txn.take_header()), + (&txn).into(), ExecutionResult::from(ExecutionResultV2::random(rng)), Vec::new(), )]; @@ -311,14 +311,14 @@ mod tests { let txn1 = TransactionV1::random(rng); let execution_results1 = vec![ExecutionArtifact::new( TransactionHash::V1(*txn1.hash()), - TransactionHeader::V1(txn1.take_header()), + (&txn1).into(), ExecutionResult::from(ExecutionResultV2::random(rng)), Vec::new(), )]; let txn2 = TransactionV1::random(rng); let execution_results2 = vec![ExecutionArtifact::new( TransactionHash::V1(*txn2.hash()), - TransactionHeader::V1(txn2.take_header()), + (&txn2).into(), ExecutionResult::from(ExecutionResultV2::random(rng)), Vec::new(), )]; diff --git a/node/src/types/transaction.rs b/node/src/types/transaction.rs index 3fa0faaa3b..1544b84ce3 100644 --- a/node/src/types/transaction.rs +++ b/node/src/types/transaction.rs @@ -1,5 +1,6 @@ mod deploy; +mod meta_transaction; mod transaction_footprint; - pub(crate) use deploy::LegacyDeploy; +pub(crate) use meta_transaction::{MetaTransaction, TransactionHeader}; pub(crate) use transaction_footprint::TransactionFootprint; diff --git a/node/src/types/transaction/meta_transaction.rs b/node/src/types/transaction/meta_transaction.rs new file mode 100644 index 0000000000..e2ff73d025 --- /dev/null +++ b/node/src/types/transaction/meta_transaction.rs @@ -0,0 +1,279 @@ +mod meta_transaction_v1; +mod tranasction_lane; +mod transaction_header; +pub(crate) use transaction_header::*; + +use casper_execution_engine::engine_state::{SessionDataDeploy, SessionDataV1, SessionInputData}; +use casper_types::{ + account::AccountHash, bytesrepr::ToBytes, Approval, Chainspec, Deploy, Digest, Gas, GasLimited, + InitiatorAddr, InvalidTransaction, Phase, PricingMode, RuntimeArgs, TimeDiff, Timestamp, + Transaction, TransactionConfig, TransactionEntryPoint, TransactionHash, TransactionTarget, + INSTALL_UPGRADE_LANE_ID, LARGE_WASM_LANE_ID, MINT_LANE_ID, +}; +use core::fmt::{self, Debug, Display, Formatter}; +#[cfg(feature = "datasize")] +use datasize::DataSize; +pub(crate) use meta_transaction_v1::MetaTransactionV1; +use serde::Serialize; +use std::collections::BTreeSet; + +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[derive(Clone, Debug, Serialize)] +pub(crate) enum MetaTransaction { + Deploy(Deploy), + V1(MetaTransactionV1), +} + +impl MetaTransaction { + /// Returns the `TransactionHash` identifying this transaction. + pub fn hash(&self) -> TransactionHash { + match self { + MetaTransaction::Deploy(deploy) => TransactionHash::from(*deploy.hash()), + MetaTransaction::V1(txn) => TransactionHash::from(*txn.hash()), + } + } + + /// Timestamp. + pub fn timestamp(&self) -> Timestamp { + match self { + MetaTransaction::Deploy(deploy) => deploy.header().timestamp(), + MetaTransaction::V1(v1) => v1.timestamp(), + } + } + + /// Time to live. + pub fn ttl(&self) -> TimeDiff { + match self { + MetaTransaction::Deploy(deploy) => deploy.header().ttl(), + MetaTransaction::V1(v1) => v1.ttl(), + } + } + + /// Returns the `Approval`s for this transaction. + pub fn approvals(&self) -> BTreeSet { + match self { + MetaTransaction::Deploy(deploy) => deploy.approvals().clone(), + MetaTransaction::V1(v1) => v1.approvals().clone(), + } + } + + /// Returns the address of the initiator of the transaction. + pub fn initiator_addr(&self) -> InitiatorAddr { + match self { + MetaTransaction::Deploy(deploy) => InitiatorAddr::PublicKey(deploy.account().clone()), + MetaTransaction::V1(txn) => txn.initiator_addr().clone(), + } + } + + /// Returns the set of account hashes corresponding to the public keys of the approvals. + pub fn signers(&self) -> BTreeSet { + match self { + MetaTransaction::Deploy(deploy) => deploy + .approvals() + .iter() + .map(|approval| approval.signer().to_account_hash()) + .collect(), + MetaTransaction::V1(txn) => txn + .approvals() + .iter() + .map(|approval| approval.signer().to_account_hash()) + .collect(), + } + } + + /// Returns `true` if `self` represents a native transfer deploy or a native V1 transaction. + pub fn is_native(&self) -> bool { + match self { + MetaTransaction::Deploy(deploy) => deploy.is_transfer(), + MetaTransaction::V1(v1_txn) => *v1_txn.target() == TransactionTarget::Native, + } + } + + /// Should this transaction use standard payment processing? + pub fn is_standard_payment(&self) -> bool { + match self { + MetaTransaction::Deploy(deploy) => deploy.payment().is_standard_payment(Phase::Payment), + MetaTransaction::V1(v1) => { + if let PricingMode::Classic { + standard_payment, .. + } = v1.pricing_mode() + { + *standard_payment + } else { + true + } + } + } + } + + /// Authorization keys. + pub fn authorization_keys(&self) -> BTreeSet { + match self { + MetaTransaction::Deploy(deploy) => deploy + .approvals() + .iter() + .map(|approval| approval.signer().to_account_hash()) + .collect(), + MetaTransaction::V1(transaction_v1) => transaction_v1 + .approvals() + .iter() + .map(|approval| approval.signer().to_account_hash()) + .collect(), + } + } + + /// The session args. + pub fn session_args(&self) -> &RuntimeArgs { + match self { + MetaTransaction::Deploy(deploy) => deploy.session().args(), + MetaTransaction::V1(transaction_v1) => transaction_v1.args(), + } + } + + /// The entry point. + pub fn entry_point(&self) -> TransactionEntryPoint { + match self { + MetaTransaction::Deploy(deploy) => deploy.session().entry_point_name().into(), + MetaTransaction::V1(transaction_v1) => transaction_v1.entry_point().clone(), + } + } + + /// The transaction lane. + pub fn transaction_lane(&self) -> u8 { + match self { + MetaTransaction::Deploy(deploy) => { + if deploy.is_transfer() { + MINT_LANE_ID + } else { + LARGE_WASM_LANE_ID + } + } + MetaTransaction::V1(v1) => v1.transaction_lane(), + } + } + + /// Returns the gas price tolerance. + pub fn gas_price_tolerance(&self) -> Result { + match self { + MetaTransaction::Deploy(deploy) => deploy + .gas_price_tolerance() + .map_err(InvalidTransaction::from), + MetaTransaction::V1(v1) => Ok(v1.gas_price_tolerance()), + } + } + + pub fn gas_limit(&self, chainspec: &Chainspec) -> Result { + match self { + MetaTransaction::Deploy(deploy) => deploy + .gas_limit(chainspec) + .map_err(InvalidTransaction::from), + MetaTransaction::V1(v1) => v1.gas_limit(chainspec), + } + } + + /// Is the transaction the legacy deploy variant. + pub fn is_legacy_transaction(&self) -> bool { + match self { + MetaTransaction::Deploy(_) => true, + MetaTransaction::V1(_) => false, + } + } + + pub fn from( + transaction: &Transaction, + transaction_config: &TransactionConfig, + ) -> Result { + match transaction { + Transaction::Deploy(deploy) => Ok(MetaTransaction::Deploy(deploy.clone())), + Transaction::V1(v1) => { + MetaTransactionV1::from(v1, transaction_config).map(MetaTransaction::V1) + } + } + } + + pub fn is_config_compliant( + &self, + chainspec: &Chainspec, + timestamp_leeway: TimeDiff, + at: Timestamp, + ) -> Result<(), InvalidTransaction> { + match self { + MetaTransaction::Deploy(deploy) => deploy + .is_config_compliant(chainspec, timestamp_leeway, at) + .map_err(InvalidTransaction::from), + MetaTransaction::V1(v1) => v1 + .is_config_compliant(chainspec, timestamp_leeway, at) + .map_err(InvalidTransaction::from), + } + } + + pub fn payload_hash(&self) -> Digest { + match self { + MetaTransaction::Deploy(deploy) => *deploy.body_hash(), + MetaTransaction::V1(v1) => *v1.payload_hash(), + } + } + + pub fn to_session_input_data(&self) -> SessionInputData { + let initiator_addr = self.initiator_addr(); + let is_standard_payment = self.is_standard_payment(); + match self { + MetaTransaction::Deploy(deploy) => { + let data = SessionDataDeploy::new( + deploy.hash(), + deploy.session(), + initiator_addr, + self.signers().clone(), + is_standard_payment, + ); + SessionInputData::DeploySessionData { data } + } + MetaTransaction::V1(v1) => { + let data = SessionDataV1::new( + v1.args(), + v1.target(), + v1.entry_point(), + v1.transaction_lane() == INSTALL_UPGRADE_LANE_ID, + v1.hash(), + v1.pricing_mode(), + initiator_addr, + self.signers().clone(), + is_standard_payment, + ); + SessionInputData::SessionDataV1 { data } + } + } + } + + /// Size estimate. + pub fn size_estimate(&self) -> usize { + match self { + MetaTransaction::Deploy(deploy) => deploy.serialized_length(), + MetaTransaction::V1(v1) => v1.serialized_length(), + } + } +} + +impl Display for MetaTransaction { + fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { + match self { + MetaTransaction::Deploy(deploy) => Display::fmt(deploy, formatter), + MetaTransaction::V1(txn) => Display::fmt(txn, formatter), + } + } +} + +#[cfg(test)] +mod proptests { + use super::*; + use casper_types::gens::legal_transaction_arb; + use proptest::prelude::*; + + proptest! { + #[test] + fn construction_roundtrip(transaction in legal_transaction_arb()) { + let maybe_transaction = MetaTransaction::from(&transaction, &TransactionConfig::default()); + assert!(maybe_transaction.is_ok()); + } + } +} diff --git a/node/src/types/transaction/meta_transaction/meta_transaction_v1.rs b/node/src/types/transaction/meta_transaction/meta_transaction_v1.rs new file mode 100644 index 0000000000..ea2194d169 --- /dev/null +++ b/node/src/types/transaction/meta_transaction/meta_transaction_v1.rs @@ -0,0 +1,594 @@ +use super::tranasction_lane::{calculate_transaction_lane, TransactionLane}; +use casper_types::{ + arg_handling, bytesrepr::ToBytes, crypto, Approval, Chainspec, Digest, DisplayIter, Gas, + InitiatorAddr, InvalidTransaction, InvalidTransactionV1, PricingHandling, PricingMode, + RuntimeArgs, TimeDiff, Timestamp, TransactionConfig, TransactionEntryPoint, + TransactionScheduling, TransactionTarget, TransactionV1, TransactionV1ExcessiveSizeError, + TransactionV1Hash, U512, +}; +use core::fmt::{self, Debug, Display, Formatter}; +#[cfg(feature = "datasize")] +use datasize::DataSize; +#[cfg(any(feature = "once_cell", test))] +use once_cell::sync::OnceCell; +use serde::Serialize; +use std::collections::BTreeSet; +use tracing::debug; + +const ARGS_MAP_KEY: u16 = 0; +const TARGET_MAP_KEY: u16 = 1; +const ENTRY_POINT_MAP_KEY: u16 = 2; +const SCHEDULING_MAP_KEY: u16 = 3; +const EXPECTED_NUMBER_OF_FIELDS: usize = 4; + +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[derive(Clone, Debug, Serialize)] +pub struct MetaTransactionV1 { + hash: TransactionV1Hash, + chain_name: String, + timestamp: Timestamp, + ttl: TimeDiff, + pricing_mode: PricingMode, + initiator_addr: InitiatorAddr, + args: RuntimeArgs, + target: TransactionTarget, + entry_point: TransactionEntryPoint, + transaction_lane: TransactionLane, + scheduling: TransactionScheduling, + approvals: BTreeSet, + serialized_length: usize, + payload_hash: Digest, + has_valid_hash: Result<(), InvalidTransactionV1>, + #[cfg_attr(any(all(feature = "std", feature = "once_cell"), test), serde(skip))] + #[cfg_attr( + all(any(feature = "once_cell", test), feature = "datasize"), + data_size(skip) + )] + #[cfg(any(feature = "once_cell", test))] + is_verified: OnceCell>, +} + +impl MetaTransactionV1 { + pub fn from( + v1: &TransactionV1, + transaction_config: &TransactionConfig, + ) -> Result { + let args: RuntimeArgs = v1.deserialize_field(ARGS_MAP_KEY).map_err(|error| { + InvalidTransaction::V1(InvalidTransactionV1::CouldNotDeserializeField { error }) + })?; + let target: TransactionTarget = v1.deserialize_field(TARGET_MAP_KEY).map_err(|error| { + InvalidTransaction::V1(InvalidTransactionV1::CouldNotDeserializeField { error }) + })?; + let entry_point: TransactionEntryPoint = + v1.deserialize_field(ENTRY_POINT_MAP_KEY).map_err(|error| { + InvalidTransaction::V1(InvalidTransactionV1::CouldNotDeserializeField { error }) + })?; + let scheduling: TransactionScheduling = + v1.deserialize_field(SCHEDULING_MAP_KEY).map_err(|error| { + InvalidTransaction::V1(InvalidTransactionV1::CouldNotDeserializeField { error }) + })?; + + if v1.number_of_fields() != EXPECTED_NUMBER_OF_FIELDS { + return Err(InvalidTransaction::V1( + InvalidTransactionV1::UnexpectedTransactionFieldEntries, + )); + } + + let payload_hash = v1.payload_hash()?; + let serialized_length = v1.serialized_length(); + + let lane_id = calculate_transaction_lane( + &entry_point, + &target, + v1.pricing_mode().additional_computation_factor(), + transaction_config, + serialized_length as u64, + )?; + let transaction_lane = + TransactionLane::try_from(lane_id).map_err(Into::::into)?; + let has_valid_hash = v1.has_valid_hash(); + Ok(MetaTransactionV1::new( + *v1.hash(), + v1.chain_name().to_string(), + v1.timestamp(), + v1.ttl(), + v1.pricing_mode().clone(), + v1.initiator_addr().clone(), + args, + target, + entry_point, + transaction_lane, + scheduling, + serialized_length, + payload_hash, + v1.approvals().clone(), + has_valid_hash, + )) + } + + #[allow(clippy::too_many_arguments)] + pub fn new( + hash: TransactionV1Hash, + chain_name: String, + timestamp: Timestamp, + ttl: TimeDiff, + pricing_mode: PricingMode, + initiator_addr: InitiatorAddr, + args: RuntimeArgs, + target: TransactionTarget, + entry_point: TransactionEntryPoint, + transaction_lane: TransactionLane, + scheduling: TransactionScheduling, + serialized_length: usize, + payload_hash: Digest, + approvals: BTreeSet, + has_valid_hash: Result<(), InvalidTransactionV1>, + ) -> Self { + Self { + hash, + chain_name, + timestamp, + ttl, + pricing_mode, + initiator_addr, + args, + target, + entry_point, + transaction_lane, + scheduling, + approvals, + serialized_length, + payload_hash, + has_valid_hash, + #[cfg(any(feature = "once_cell", test))] + is_verified: OnceCell::new(), + } + } + + /// Returns the runtime args of the transaction. + pub fn args(&self) -> &RuntimeArgs { + &self.args + } + + /// Returns the `DeployHash` identifying this `Deploy`. + pub fn hash(&self) -> &TransactionV1Hash { + &self.hash + } + + /// Returns the `Approvals`. + pub fn approvals(&self) -> &BTreeSet { + &self.approvals + } + + /// Returns `Ok` if and only if: + /// * the transaction hash is correct (see [`TransactionV1::has_valid_hash`] for details) + /// * approvals are non empty, and + /// * all approvals are valid signatures of the signed hash + pub fn verify(&self) -> Result<(), InvalidTransactionV1> { + #[cfg(any(feature = "once_cell", test))] + return self.is_verified.get_or_init(|| self.do_verify()).clone(); + + #[cfg(not(any(feature = "once_cell", test)))] + self.do_verify() + } + + /// Returns `Ok` if and only if this transaction's body hashes to the value of `body_hash()`, + /// and if this transaction's header hashes to the value claimed as the transaction hash. + pub fn has_valid_hash(&self) -> &Result<(), InvalidTransactionV1> { + &self.has_valid_hash + } + + fn do_verify(&self) -> Result<(), InvalidTransactionV1> { + if self.approvals.is_empty() { + debug!(?self, "transaction has no approvals"); + return Err(InvalidTransactionV1::EmptyApprovals); + } + + self.has_valid_hash().clone()?; + + for (index, approval) in self.approvals.iter().enumerate() { + if let Err(error) = crypto::verify(self.hash, approval.signature(), approval.signer()) { + debug!( + ?self, + "failed to verify transaction approval {}: {}", index, error + ); + return Err(InvalidTransactionV1::InvalidApproval { index, error }); + } + } + + Ok(()) + } + + /// Returns the entry point of the transaction. + pub fn entry_point(&self) -> &TransactionEntryPoint { + &self.entry_point + } + + /// Returns the transaction lane. + pub fn transaction_lane(&self) -> u8 { + self.transaction_lane as u8 + } + + /// Returns payload hash of the transaction. + pub fn payload_hash(&self) -> &Digest { + &self.payload_hash + } + + /// Returns the pricing mode for the transaction. + pub fn pricing_mode(&self) -> &PricingMode { + &self.pricing_mode + } + + /// Returns the initiator_addr of the transaction. + pub fn initiator_addr(&self) -> &InitiatorAddr { + &self.initiator_addr + } + + /// Returns the target of the transaction. + pub fn target(&self) -> &TransactionTarget { + &self.target + } + + /// Returns `true` if the serialized size of the transaction is not greater than + /// `max_transaction_size`. + fn is_valid_size( + &self, + max_transaction_size: u32, + ) -> Result<(), TransactionV1ExcessiveSizeError> { + let actual_transaction_size = self.serialized_length; + if actual_transaction_size > max_transaction_size as usize { + return Err(TransactionV1ExcessiveSizeError { + max_transaction_size, + actual_transaction_size, + }); + } + Ok(()) + } + + /// Returns the creation timestamp of the `Deploy`. + pub fn timestamp(&self) -> Timestamp { + self.timestamp + } + + /// Returns the duration after the creation timestamp for which the `Deploy` will stay valid. + /// + /// After this duration has ended, the `Deploy` will be considered expired. + pub fn ttl(&self) -> TimeDiff { + self.ttl + } + + /// Returns `Ok` if and only if: + /// * the chain_name is correct, + /// * the configured parameters are complied with at the given timestamp + pub fn is_config_compliant( + &self, + chainspec: &Chainspec, + timestamp_leeway: TimeDiff, + at: Timestamp, + ) -> Result<(), InvalidTransactionV1> { + let transaction_config = chainspec.transaction_config.clone(); + self.is_valid_size( + transaction_config + .transaction_v1_config + .get_max_serialized_length(self.transaction_lane as u8) as u32, + )?; + + let chain_name = chainspec.network_config.name.clone(); + + if self.chain_name != chain_name { + debug!( + transaction_hash = %self.hash(), + chain_name = %self.chain_name, + timestamp= %self.timestamp, + ttl= %self.ttl, + pricing_mode= %self.pricing_mode, + initiator_addr= %self.initiator_addr, + target= %self.target, + entry_point= %self.entry_point, + transaction_lane= %self.transaction_lane, + scheduling= %self.scheduling, + "invalid chain identifier" + ); + return Err(InvalidTransactionV1::InvalidChainName { + expected: chain_name, + got: self.chain_name.to_string(), + }); + } + + let price_handling = chainspec.core_config.pricing_handling; + let pricing_mode = &self.pricing_mode; + + match pricing_mode { + PricingMode::Classic { .. } => { + if let PricingHandling::Classic = price_handling { + } else { + return Err(InvalidTransactionV1::InvalidPricingMode { + price_mode: pricing_mode.clone(), + }); + } + } + PricingMode::Fixed { .. } => { + if let PricingHandling::Fixed = price_handling { + } else { + return Err(InvalidTransactionV1::InvalidPricingMode { + price_mode: pricing_mode.clone(), + }); + } + } + PricingMode::Reserved { .. } => { + if !chainspec.core_config.allow_reservations { + // Currently Reserved isn't implemented and we should + // not be accepting transactions with this mode. + return Err(InvalidTransactionV1::InvalidPricingMode { + price_mode: pricing_mode.clone(), + }); + } + } + } + + let min_gas_price = chainspec.vacancy_config.min_gas_price; + let gas_price_tolerance = self.gas_price_tolerance(); + if gas_price_tolerance < min_gas_price { + return Err(InvalidTransactionV1::GasPriceToleranceTooLow { + min_gas_price_tolerance: min_gas_price, + provided_gas_price_tolerance: gas_price_tolerance, + }); + } + + self.is_header_metadata_valid(&transaction_config, timestamp_leeway, at, &self.hash)?; + + let max_associated_keys = chainspec.core_config.max_associated_keys; + + if self.approvals.len() > max_associated_keys as usize { + debug!( + transaction_hash = %self.hash(), + number_of_approvals = %self.approvals.len(), + max_associated_keys = %max_associated_keys, + "number of transaction approvals exceeds the limit" + ); + return Err(InvalidTransactionV1::ExcessiveApprovals { + got: self.approvals.len() as u32, + max_associated_keys, + }); + } + + let gas_limit = self + .pricing_mode + .gas_limit(chainspec, &self.entry_point, self.transaction_lane as u8) + .map_err(Into::::into)?; + let block_gas_limit = Gas::new(U512::from(transaction_config.block_gas_limit)); + if gas_limit > block_gas_limit { + debug!( + amount = %gas_limit, + %block_gas_limit, + "transaction gas limit exceeds block gas limit" + ); + return Err(InvalidTransactionV1::ExceedsBlockGasLimit { + block_gas_limit: transaction_config.block_gas_limit, + got: Box::new(gas_limit.value()), + }); + } + + self.is_body_metadata_valid(&transaction_config) + } + + fn is_body_metadata_valid( + &self, + config: &TransactionConfig, + ) -> Result<(), InvalidTransactionV1> { + let lane_id = self.transaction_lane as u8; + if !config.transaction_v1_config.is_supported(lane_id) { + return Err(InvalidTransactionV1::InvalidTransactionLane(lane_id)); + } + + let max_serialized_length = config + .transaction_v1_config + .get_max_serialized_length(lane_id); + let actual_length = self.serialized_length; + if actual_length > max_serialized_length as usize { + return Err(InvalidTransactionV1::ExcessiveSize( + TransactionV1ExcessiveSizeError { + max_transaction_size: max_serialized_length as u32, + actual_transaction_size: actual_length, + }, + )); + } + + let max_args_length = config.transaction_v1_config.get_max_args_length(lane_id); + + let args_length = self.args.serialized_length(); + if args_length > max_args_length as usize { + debug!( + args_length, + max_args_length = max_args_length, + "transaction runtime args excessive size" + ); + return Err(InvalidTransactionV1::ExcessiveArgsLength { + max_length: max_args_length as usize, + got: args_length, + }); + } + + match &self.target { + TransactionTarget::Native => match self.entry_point { + TransactionEntryPoint::Call => { + debug!( + entry_point = %self.entry_point, + "native transaction cannot have call entry point" + ); + Err(InvalidTransactionV1::EntryPointCannotBeCall) + } + TransactionEntryPoint::Custom(_) => { + debug!( + entry_point = %self.entry_point, + "native transaction cannot have custom entry point" + ); + Err(InvalidTransactionV1::EntryPointCannotBeCustom { + entry_point: self.entry_point.clone(), + }) + } + TransactionEntryPoint::Transfer => arg_handling::has_valid_transfer_args( + &self.args, + config.native_transfer_minimum_motes, + ), + TransactionEntryPoint::AddBid => arg_handling::has_valid_add_bid_args(&self.args), + TransactionEntryPoint::WithdrawBid => { + arg_handling::has_valid_withdraw_bid_args(&self.args) + } + TransactionEntryPoint::Delegate => { + arg_handling::has_valid_delegate_args(&self.args) + } + TransactionEntryPoint::Undelegate => { + arg_handling::has_valid_undelegate_args(&self.args) + } + TransactionEntryPoint::Redelegate => { + arg_handling::has_valid_redelegate_args(&self.args) + } + TransactionEntryPoint::ActivateBid => { + arg_handling::has_valid_activate_bid_args(&self.args) + } + TransactionEntryPoint::ChangeBidPublicKey => { + arg_handling::has_valid_change_bid_public_key_args(&self.args) + } + TransactionEntryPoint::AddReservations => { + todo!() + } + TransactionEntryPoint::CancelReservations => { + todo!() + } + }, + TransactionTarget::Stored { .. } => match &self.entry_point { + TransactionEntryPoint::Custom(_) => Ok(()), + TransactionEntryPoint::Call + | TransactionEntryPoint::Transfer + | TransactionEntryPoint::AddBid + | TransactionEntryPoint::WithdrawBid + | TransactionEntryPoint::Delegate + | TransactionEntryPoint::Undelegate + | TransactionEntryPoint::Redelegate + | TransactionEntryPoint::ActivateBid + | TransactionEntryPoint::ChangeBidPublicKey + | TransactionEntryPoint::AddReservations + | TransactionEntryPoint::CancelReservations => { + debug!( + entry_point = %self.entry_point, + "transaction targeting stored entity/package must have custom entry point" + ); + Err(InvalidTransactionV1::EntryPointMustBeCustom { + entry_point: self.entry_point.clone(), + }) + } + }, + TransactionTarget::Session { module_bytes, .. } => match &self.entry_point { + TransactionEntryPoint::Call | TransactionEntryPoint::Custom(_) => { + if module_bytes.is_empty() { + debug!("transaction with session code must not have empty module bytes"); + return Err(InvalidTransactionV1::EmptyModuleBytes); + } + Ok(()) + } + TransactionEntryPoint::Transfer + | TransactionEntryPoint::AddBid + | TransactionEntryPoint::WithdrawBid + | TransactionEntryPoint::Delegate + | TransactionEntryPoint::Undelegate + | TransactionEntryPoint::Redelegate + | TransactionEntryPoint::ActivateBid + | TransactionEntryPoint::ChangeBidPublicKey + | TransactionEntryPoint::AddReservations + | TransactionEntryPoint::CancelReservations => { + debug!( + entry_point = %self.entry_point, + "transaction with session code must use custom or default 'call' entry point" + ); + Err(InvalidTransactionV1::EntryPointMustBeCustom { + entry_point: self.entry_point.clone(), + }) + } + }, + } + } + + fn is_header_metadata_valid( + &self, + config: &TransactionConfig, + timestamp_leeway: TimeDiff, + at: Timestamp, + transaction_hash: &TransactionV1Hash, + ) -> Result<(), InvalidTransactionV1> { + if self.ttl() > config.max_ttl { + debug!( + %transaction_hash, + transaction_header = %self, + max_ttl = %config.max_ttl, + "transaction ttl excessive" + ); + return Err(InvalidTransactionV1::ExcessiveTimeToLive { + max_ttl: config.max_ttl, + got: self.ttl(), + }); + } + + if self.timestamp() > at + timestamp_leeway { + debug!( + %transaction_hash, transaction_header = %self, %at, + "transaction timestamp in the future" + ); + return Err(InvalidTransactionV1::TimestampInFuture { + validation_timestamp: at, + timestamp_leeway, + got: self.timestamp(), + }); + } + + Ok(()) + } + + /// Returns the gas price tolerance for the given transaction. + pub fn gas_price_tolerance(&self) -> u8 { + match self.pricing_mode { + PricingMode::Classic { + gas_price_tolerance, + .. + } => gas_price_tolerance, + PricingMode::Fixed { + gas_price_tolerance, + .. + } => gas_price_tolerance, + PricingMode::Reserved { .. } => { + // TODO: Change this when reserve gets implemented. + 0u8 + } + } + } + + pub fn serialized_length(&self) -> usize { + self.serialized_length + } + + pub fn gas_limit(&self, chainspec: &Chainspec) -> Result { + self.pricing_mode() + .gas_limit(chainspec, self.entry_point(), self.transaction_lane as u8) + .map_err(Into::into) + } +} + +impl Display for MetaTransactionV1 { + fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { + write!( + formatter, + "meta-transaction-v1[hash: {}, chain_name: {}, timestamp: {}, ttl: {}, pricing_mode: {}, initiator_addr: {}, target: {}, entry_point: {}, transaction_lane: {}, scheduling: {}, approvals: {}]", + self.hash, + self.chain_name, + self.timestamp, + self.ttl, + self.pricing_mode, + self.initiator_addr, + self.target, + self.entry_point, + self.transaction_lane, + self.scheduling, + DisplayIter::new(self.approvals.iter()) + ) + } +} diff --git a/node/src/types/transaction/meta_transaction/tranasction_lane.rs b/node/src/types/transaction/meta_transaction/tranasction_lane.rs new file mode 100644 index 0000000000..0d39ccf0f5 --- /dev/null +++ b/node/src/types/transaction/meta_transaction/tranasction_lane.rs @@ -0,0 +1,161 @@ +use core::{ + convert::TryFrom, + fmt::{self, Formatter}, +}; + +use casper_types::{ + InvalidTransaction, InvalidTransactionV1, TransactionConfig, TransactionEntryPoint, + TransactionTarget, TransactionV1Config, AUCTION_LANE_ID, INSTALL_UPGRADE_LANE_ID, MINT_LANE_ID, +}; +#[cfg(feature = "datasize")] +use datasize::DataSize; +use serde::Serialize; + +/// The category of a Transaction. +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Serialize)] +#[repr(u8)] +pub enum TransactionLane { + /// Native mint interaction (the default). + Mint = 0, + /// Native auction interaction. + Auction = 1, + /// InstallUpgradeWasm + InstallUpgradeWasm = 2, + /// A large Wasm based transaction. + Large = 3, + /// A medium Wasm based transaction. + Medium = 4, + /// A small Wasm based transaction. + Small = 5, +} + +#[derive(Debug)] +pub struct TransactionLaneConversionError(u8); + +impl From for InvalidTransaction { + fn from(value: TransactionLaneConversionError) -> InvalidTransaction { + InvalidTransaction::V1(InvalidTransactionV1::InvalidTransactionLane(value.0)) + } +} + +impl TryFrom for TransactionLane { + type Error = TransactionLaneConversionError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Mint), + 1 => Ok(Self::Auction), + 2 => Ok(Self::InstallUpgradeWasm), + 3 => Ok(Self::Large), + 4 => Ok(Self::Medium), + 5 => Ok(Self::Small), + _ => Err(TransactionLaneConversionError(value)), + } + } +} + +impl fmt::Display for TransactionLane { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + TransactionLane::Mint => write!(f, "Mint"), + TransactionLane::Auction => write!(f, "Auction"), + TransactionLane::Large => write!(f, "Large"), + TransactionLane::Medium => write!(f, "Medium"), + TransactionLane::Small => write!(f, "Small"), + TransactionLane::InstallUpgradeWasm => write!(f, "InstallUpgradeWASM"), + } + } +} + +/// Calculates the laned based on properties of the transaction +pub(crate) fn calculate_transaction_lane( + entry_point: &TransactionEntryPoint, + target: &TransactionTarget, + additional_computation_factor: u8, + transaction_config: &TransactionConfig, + transaction_size: u64, +) -> Result { + match target { + TransactionTarget::Native => match entry_point { + TransactionEntryPoint::Transfer => Ok(MINT_LANE_ID), + TransactionEntryPoint::AddBid + | TransactionEntryPoint::WithdrawBid + | TransactionEntryPoint::Delegate + | TransactionEntryPoint::Undelegate + | TransactionEntryPoint::Redelegate + | TransactionEntryPoint::ActivateBid + | TransactionEntryPoint::ChangeBidPublicKey + | TransactionEntryPoint::AddReservations + | TransactionEntryPoint::CancelReservations => Ok(AUCTION_LANE_ID), + TransactionEntryPoint::Call => Err(InvalidTransactionV1::EntryPointCannotBeCall), + TransactionEntryPoint::Custom(_) => { + Err(InvalidTransactionV1::EntryPointCannotBeCustom { + entry_point: entry_point.clone(), + }) + } + }, + TransactionTarget::Stored { .. } => match entry_point { + TransactionEntryPoint::Custom(_) => get_lane_for_non_install_wasm( + &transaction_config.transaction_v1_config, + transaction_size, + additional_computation_factor, + ), + TransactionEntryPoint::Call + | TransactionEntryPoint::Transfer + | TransactionEntryPoint::AddBid + | TransactionEntryPoint::WithdrawBid + | TransactionEntryPoint::Delegate + | TransactionEntryPoint::Undelegate + | TransactionEntryPoint::Redelegate + | TransactionEntryPoint::ActivateBid + | TransactionEntryPoint::ChangeBidPublicKey + | TransactionEntryPoint::AddReservations + | TransactionEntryPoint::CancelReservations => { + Err(InvalidTransactionV1::EntryPointMustBeCustom { + entry_point: entry_point.clone(), + }) + } + }, + TransactionTarget::Session { + is_install_upgrade, .. + } => match entry_point { + TransactionEntryPoint::Call => { + if *is_install_upgrade { + Ok(INSTALL_UPGRADE_LANE_ID) + } else { + get_lane_for_non_install_wasm( + &transaction_config.transaction_v1_config, + transaction_size, + additional_computation_factor, + ) + } + } + TransactionEntryPoint::Custom(_) + | TransactionEntryPoint::Transfer + | TransactionEntryPoint::AddBid + | TransactionEntryPoint::WithdrawBid + | TransactionEntryPoint::Delegate + | TransactionEntryPoint::Undelegate + | TransactionEntryPoint::Redelegate + | TransactionEntryPoint::ActivateBid + | TransactionEntryPoint::ChangeBidPublicKey + | TransactionEntryPoint::AddReservations + | TransactionEntryPoint::CancelReservations => { + Err(InvalidTransactionV1::EntryPointMustBeCall { + entry_point: entry_point.clone(), + }) + } + }, + } +} + +fn get_lane_for_non_install_wasm( + config: &TransactionV1Config, + transaction_size: u64, + additional_computation_factor: u8, +) -> Result { + config + .get_wasm_lane_id(transaction_size, additional_computation_factor) + .ok_or(InvalidTransactionV1::NoWasmLaneMatchesTransaction()) +} diff --git a/node/src/types/transaction/meta_transaction/transaction_header.rs b/node/src/types/transaction/meta_transaction/transaction_header.rs new file mode 100644 index 0000000000..fa0c6b0108 --- /dev/null +++ b/node/src/types/transaction/meta_transaction/transaction_header.rs @@ -0,0 +1,77 @@ +use casper_types::{DeployHeader, InitiatorAddr, TimeDiff, Timestamp, Transaction, TransactionV1}; +use core::fmt::{self, Display, Formatter}; +use datasize::DataSize; +use serde::Serialize; + +#[derive(Debug, Clone, DataSize, PartialEq, Eq, Serialize)] +pub(crate) struct TransactionV1Metadata { + initiator_addr: InitiatorAddr, + timestamp: Timestamp, + ttl: TimeDiff, +} + +impl TransactionV1Metadata { + pub(crate) fn initiator_addr(&self) -> &InitiatorAddr { + &self.initiator_addr + } + + pub(crate) fn timestamp(&self) -> Timestamp { + self.timestamp + } + + pub(crate) fn ttl(&self) -> TimeDiff { + self.ttl + } +} + +impl Display for TransactionV1Metadata { + fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { + write!( + formatter, + "transaction-v1-metadata[initiator_addr: {}]", + self.initiator_addr, + ) + } +} + +#[derive(Debug, Clone, DataSize, Serialize, PartialEq, Eq)] +/// A versioned wrapper for a transaction header or deploy header. +pub(crate) enum TransactionHeader { + Deploy(DeployHeader), + V1(TransactionV1Metadata), +} + +impl From for TransactionHeader { + fn from(header: DeployHeader) -> Self { + Self::Deploy(header) + } +} + +impl From<&TransactionV1> for TransactionHeader { + fn from(transaction_v1: &TransactionV1) -> Self { + let meta = TransactionV1Metadata { + initiator_addr: transaction_v1.initiator_addr().clone(), + timestamp: transaction_v1.timestamp(), + ttl: transaction_v1.ttl(), + }; + Self::V1(meta) + } +} + +impl From<&Transaction> for TransactionHeader { + fn from(transaction: &Transaction) -> Self { + match transaction { + Transaction::Deploy(deploy) => deploy.header().clone().into(), + Transaction::V1(v1) => v1.into(), + } + } +} + +impl Display for TransactionHeader { + fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { + match self { + TransactionHeader::Deploy(header) => Display::fmt(header, formatter), + TransactionHeader::V1(meta) => Display::fmt(meta, formatter), + } + } +} diff --git a/node/src/types/transaction/transaction_footprint.rs b/node/src/types/transaction/transaction_footprint.rs index 163c6104b2..825fa57589 100644 --- a/node/src/types/transaction/transaction_footprint.rs +++ b/node/src/types/transaction/transaction_footprint.rs @@ -1,6 +1,7 @@ +use crate::types::MetaTransaction; use casper_types::{ - Approval, Chainspec, Digest, Gas, GasLimited, InvalidTransaction, InvalidTransactionV1, - TimeDiff, Timestamp, Transaction, TransactionHash, AUCTION_LANE_ID, INSTALL_UPGRADE_LANE_ID, + Approval, Chainspec, Digest, Gas, InvalidTransaction, InvalidTransactionV1, TimeDiff, + Timestamp, Transaction, TransactionHash, AUCTION_LANE_ID, INSTALL_UPGRADE_LANE_ID, MINT_LANE_ID, }; use datasize::DataSize; @@ -13,16 +14,16 @@ use std::collections::BTreeSet; pub(crate) struct TransactionFootprint { /// The identifying hash. pub(crate) transaction_hash: TransactionHash, - /// Transaction body hash. - pub(crate) body_hash: Digest, + /// Transaction payload hash. + pub(crate) payload_hash: Digest, /// The estimated gas consumption. pub(crate) gas_limit: Gas, /// The gas tolerance. pub(crate) gas_price_tolerance: u8, /// The bytesrepr serialized length. pub(crate) size_estimate: usize, - /// The transaction category. - pub(crate) category: u8, + /// The transaction lane_id. + pub(crate) lane_id: u8, /// Timestamp of the transaction. pub(crate) timestamp: Timestamp, /// Time to live for the transaction. @@ -35,32 +36,40 @@ impl TransactionFootprint { pub(crate) fn new( chainspec: &Chainspec, transaction: &Transaction, + ) -> Result { + let transaction = MetaTransaction::from(transaction, &chainspec.transaction_config)?; + Self::new_from_meta_transaction(chainspec, &transaction) + } + + fn new_from_meta_transaction( + chainspec: &Chainspec, + transaction: &MetaTransaction, ) -> Result { let gas_price_tolerance = transaction.gas_price_tolerance()?; let gas_limit = transaction.gas_limit(chainspec)?; - let category = transaction.transaction_category(); + let lane_id = transaction.transaction_lane(); if !chainspec .transaction_config .transaction_v1_config - .is_supported(category) + .is_supported(lane_id) { return Err(InvalidTransaction::V1( - InvalidTransactionV1::InvalidTransactionKind(category), + InvalidTransactionV1::InvalidTransactionLane(lane_id), )); } let transaction_hash = transaction.hash(); - let body_hash = transaction.body_hash(); let size_estimate = transaction.size_estimate(); + let payload_hash = transaction.payload_hash(); let timestamp = transaction.timestamp(); let ttl = transaction.ttl(); let approvals = transaction.approvals(); Ok(TransactionFootprint { transaction_hash, - body_hash, + payload_hash, gas_limit, gas_price_tolerance, size_estimate, - category, + lane_id, timestamp, ttl, approvals, @@ -80,7 +89,7 @@ impl TransactionFootprint { /// Is mint interaction. pub(crate) fn is_mint(&self) -> bool { - if self.category == MINT_LANE_ID { + if self.lane_id == MINT_LANE_ID { return true; } @@ -89,7 +98,7 @@ impl TransactionFootprint { /// Is auction interaction. pub(crate) fn is_auction(&self) -> bool { - if self.category == AUCTION_LANE_ID { + if self.lane_id == AUCTION_LANE_ID { return true; } @@ -97,7 +106,7 @@ impl TransactionFootprint { } pub(crate) fn is_install_upgrade(&self) -> bool { - if self.category == INSTALL_UPGRADE_LANE_ID { + if self.lane_id == INSTALL_UPGRADE_LANE_ID { return true; } diff --git a/node/src/utils/specimen.rs b/node/src/utils/specimen.rs index d0a30fb897..cbf84eca64 100644 --- a/node/src/utils/specimen.rs +++ b/node/src/utils/specimen.rs @@ -28,7 +28,7 @@ use casper_types::{ ProtocolVersion, RewardedSignatures, RuntimeArgs, SecretKey, SemVer, SignedBlockHeader, SingleBlockRewardedSignatures, TimeDiff, Timestamp, Transaction, TransactionHash, TransactionId, TransactionV1, TransactionV1Builder, TransactionV1Hash, URef, AUCTION_LANE_ID, - INSTALL_UPGRADE_LANE_ID, KEY_HASH_LENGTH, MINT_LANE_ID, U512, + INSTALL_UPGRADE_LANE_ID, KEY_HASH_LENGTH, LARGE_WASM_LANE_ID, MINT_LANE_ID, U512, }; use crate::{ @@ -47,8 +47,6 @@ use casper_storage::block_store::types::ApprovalsHashes; /// The largest valid unicode codepoint that can be encoded to UTF-8. pub(crate) const HIGHEST_UNICODE_CODEPOINT: char = '\u{10FFFF}'; -const LARGE_LANE_ID: u8 = 3; - /// A cache used for memoization, typically on a single estimator. #[derive(Debug, Default)] pub(crate) struct Cache { @@ -860,7 +858,7 @@ impl LargestSpecimen for BlockPayload { ], ); transactions.insert( - LARGE_LANE_ID, + LARGE_WASM_LANE_ID, vec![ large_txn_hash_with_approvals.clone(); estimator.parameter::("max_standard_transactions_per_block") @@ -1014,9 +1012,8 @@ impl LargestSpecimen for TransactionV1 { // See comment in `impl LargestSpecimen for ExecutableDeployItem` below for rationale here. let max_size_with_margin = estimator.parameter::("max_transaction_size").max(0) as usize + 10 * 4; - TransactionV1Builder::new_session( - casper_types::TransactionCategory::InstallUpgrade, + true, Bytes::from(vec_of_largest_specimen( estimator, max_size_with_margin, diff --git a/resources/local/chainspec.toml.in b/resources/local/chainspec.toml.in index 642bc8ab16..487bce8c5e 100644 --- a/resources/local/chainspec.toml.in +++ b/resources/local/chainspec.toml.in @@ -193,7 +193,8 @@ max_timestamp_leeway = '5 seconds' # [4] -> The maximum number of transactions the lane can contain native_mint_lane = [0, 1024, 1024, 65_000_000_000, 650] native_auction_lane = [1, 2048, 2048, 362_500_000_000, 145] -wasm_lanes = [[2, 1_048_576, 2048, 1_000_000_000_000, 1], [3, 344_064, 1024, 500_000_000_000, 3], [4, 172_032, 1024, 50_000_000_000, 7], [5, 12_288, 512, 1_500_000_000, 15]] +install_upgrade_lane = [2, 1_048_576, 2048, 1_000_000_000_000, 1] +wasm_lanes = [[3, 344_064, 1024, 500_000_000_000, 3], [4, 172_032, 1024, 50_000_000_000, 7], [5, 12_288, 512, 1_500_000_000, 15]] [transactions.deploy] # The maximum number of Motes allowed to be spent during payment. 0 means unlimited. diff --git a/resources/production/chainspec.toml b/resources/production/chainspec.toml index cc12dd6267..69d665467f 100644 --- a/resources/production/chainspec.toml +++ b/resources/production/chainspec.toml @@ -203,7 +203,8 @@ max_timestamp_leeway = '5 seconds' # [4] -> The maximum number of transactions the lane can contain native_mint_lane = [0, 1024, 1024, 65_000_000_000, 650] native_auction_lane = [1, 2048, 2048, 362_500_000_000, 145] -wasm_lanes = [[2, 1_048_576, 2048, 1_000_000_000_000, 1], [3, 344_064, 1024, 500_000_000_000, 3], [4, 172_032, 1024, 50_000_000_000, 7], [5, 12_288, 512, 1_500_000_000, 15]] +install_upgrade_lane = [2, 1_048_576, 2048, 1_000_000_000_000, 1] +wasm_lanes = [[3, 344_064, 1024, 500_000_000_000, 3], [4, 172_032, 1024, 50_000_000_000, 7], [5, 12_288, 512, 1_500_000_000, 15]] [transactions.deploy] # The maximum number of Motes allowed to be spent during payment. 0 means unlimited. diff --git a/resources/test/sse_data_schema.json b/resources/test/sse_data_schema.json index 23e740922a..bf58912913 100644 --- a/resources/test/sse_data_schema.json +++ b/resources/test/sse_data_schema.json @@ -1560,25 +1560,15 @@ "type": "object", "required": [ "approvals", - "body", "hash", - "header", - "serialization_version" + "payload" ], "properties": { - "serialization_version": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, "hash": { "$ref": "#/definitions/TransactionV1Hash" }, - "header": { - "$ref": "#/definitions/TransactionV1Header" - }, - "body": { - "$ref": "#/definitions/TransactionV1Body" + "payload": { + "$ref": "#/definitions/TransactionV1Payload" }, "approvals": { "type": "array", @@ -1590,20 +1580,20 @@ }, "additionalProperties": false }, - "TransactionV1Header": { - "description": "The header portion of a TransactionV1.", + "TransactionV1Payload": { + "description": "A unit of work sent by a client to the network, which when executed can cause global state to be altered.", "type": "object", "required": [ - "body_hash", "chain_name", + "fields", "initiator_addr", "pricing_mode", "timestamp", "ttl" ], "properties": { - "chain_name": { - "type": "string" + "initiator_addr": { + "$ref": "#/definitions/InitiatorAddr" }, "timestamp": { "$ref": "#/definitions/Timestamp" @@ -1611,18 +1601,56 @@ "ttl": { "$ref": "#/definitions/TimeDiff" }, - "body_hash": { - "$ref": "#/definitions/Digest" + "chain_name": { + "type": "string" }, "pricing_mode": { "$ref": "#/definitions/PricingMode" }, - "initiator_addr": { - "$ref": "#/definitions/InitiatorAddr" + "fields": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Bytes" + } } }, "additionalProperties": false }, + "InitiatorAddr": { + "description": "The address of the initiator of a TransactionV1.", + "oneOf": [ + { + "description": "The public key of the initiator.", + "type": "object", + "required": [ + "PublicKey" + ], + "properties": { + "PublicKey": { + "$ref": "#/definitions/PublicKey" + } + }, + "additionalProperties": false + }, + { + "description": "The account hash derived from the public key of the initiator.", + "type": "object", + "required": [ + "AccountHash" + ], + "properties": { + "AccountHash": { + "$ref": "#/definitions/AccountHash" + } + }, + "additionalProperties": false + } + ] + }, + "AccountHash": { + "description": "Account hash as a formatted string.", + "type": "string" + }, "PricingMode": { "description": "Pricing mode of a Transaction.", "oneOf": [ @@ -1673,9 +1701,16 @@ "Fixed": { "type": "object", "required": [ + "additional_computation_factor", "gas_price_tolerance" ], "properties": { + "additional_computation_factor": { + "description": "User-specified additional computation factor (minimum 0). If \"0\" is provided, no additional logic is applied to the computation limit. Each value above \"0\" tells the node that it needs to treat the transaction as if it uses more gas than it's serialized size indicates. Each \"1\" will increase the \"wasm lane\" size bucket for this transaction by 1. So if the size of the transaction indicates bucket \"0\" and \"additional_computation_factor = 2\", the transaction will be treated as a \"2\".", + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, "gas_price_tolerance": { "description": "User-specified gas_price tolerance (minimum 1). This is interpreted to mean \"do not include this transaction in a block if the current gas price is greater than this number\"", "type": "integer", @@ -1717,403 +1752,6 @@ } ] }, - "InitiatorAddr": { - "description": "The address of the initiator of a TransactionV1.", - "oneOf": [ - { - "description": "The public key of the initiator.", - "type": "object", - "required": [ - "PublicKey" - ], - "properties": { - "PublicKey": { - "$ref": "#/definitions/PublicKey" - } - }, - "additionalProperties": false - }, - { - "description": "The account hash derived from the public key of the initiator.", - "type": "object", - "required": [ - "AccountHash" - ], - "properties": { - "AccountHash": { - "$ref": "#/definitions/AccountHash" - } - }, - "additionalProperties": false - } - ] - }, - "AccountHash": { - "description": "Account hash as a formatted string.", - "type": "string" - }, - "TransactionV1Body": { - "description": "Body of a `TransactionV1`.", - "type": "object", - "required": [ - "args", - "entry_point", - "scheduling", - "target", - "transaction_category" - ], - "properties": { - "args": { - "$ref": "#/definitions/RuntimeArgs" - }, - "target": { - "$ref": "#/definitions/TransactionTarget" - }, - "entry_point": { - "$ref": "#/definitions/TransactionEntryPoint" - }, - "transaction_category": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, - "scheduling": { - "$ref": "#/definitions/TransactionScheduling" - } - }, - "additionalProperties": false - }, - "TransactionTarget": { - "description": "Execution target of a Transaction.", - "oneOf": [ - { - "description": "The execution target is a native operation (e.g. a transfer).", - "type": "string", - "enum": [ - "Native" - ] - }, - { - "description": "The execution target is a stored entity or package.", - "type": "object", - "required": [ - "Stored" - ], - "properties": { - "Stored": { - "type": "object", - "required": [ - "id", - "runtime" - ], - "properties": { - "id": { - "description": "The identifier of the stored execution target.", - "allOf": [ - { - "$ref": "#/definitions/TransactionInvocationTarget" - } - ] - }, - "runtime": { - "description": "The execution runtime to use.", - "allOf": [ - { - "$ref": "#/definitions/TransactionRuntime" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "The execution target is the included module bytes, i.e. compiled Wasm.", - "type": "object", - "required": [ - "Session" - ], - "properties": { - "Session": { - "type": "object", - "required": [ - "module_bytes", - "runtime" - ], - "properties": { - "module_bytes": { - "description": "The compiled Wasm.", - "allOf": [ - { - "$ref": "#/definitions/Bytes" - } - ] - }, - "runtime": { - "description": "The execution runtime to use.", - "allOf": [ - { - "$ref": "#/definitions/TransactionRuntime" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "TransactionInvocationTarget": { - "description": "Identifier of a `Stored` transaction target.", - "oneOf": [ - { - "description": "Hex-encoded entity address identifying the invocable entity.", - "type": "object", - "required": [ - "ByHash" - ], - "properties": { - "ByHash": { - "type": "string" - } - }, - "additionalProperties": false - }, - { - "description": "The alias identifying the invocable entity.", - "type": "object", - "required": [ - "ByName" - ], - "properties": { - "ByName": { - "type": "string" - } - }, - "additionalProperties": false - }, - { - "description": "The address and optional version identifying the package.", - "type": "object", - "required": [ - "ByPackageHash" - ], - "properties": { - "ByPackageHash": { - "type": "object", - "required": [ - "addr" - ], - "properties": { - "addr": { - "description": "Hex-encoded address of the package.", - "type": "string" - }, - "version": { - "description": "The package version.\n\nIf `None`, the latest enabled version is implied.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "The alias and optional version identifying the package.", - "type": "object", - "required": [ - "ByPackageName" - ], - "properties": { - "ByPackageName": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "description": "The package name.", - "type": "string" - }, - "version": { - "description": "The package version.\n\nIf `None`, the latest enabled version is implied.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "TransactionRuntime": { - "description": "Runtime used to execute a Transaction.", - "oneOf": [ - { - "description": "The Casper Version 1 Virtual Machine.", - "type": "string", - "enum": [ - "VmCasperV1" - ] - }, - { - "description": "The Casper Version 2 Virtual Machine.", - "type": "string", - "enum": [ - "VmCasperV2" - ] - } - ] - }, - "TransactionEntryPoint": { - "description": "Entry point of a Transaction.", - "oneOf": [ - { - "description": "The standard `call` entry point used in session code.", - "type": "string", - "enum": [ - "Call" - ] - }, - { - "description": "A non-native, arbitrary entry point.", - "type": "object", - "required": [ - "Custom" - ], - "properties": { - "Custom": { - "type": "string" - } - }, - "additionalProperties": false - }, - { - "description": "The `transfer` native entry point, used to transfer `Motes` from a source purse to a target purse.", - "type": "string", - "enum": [ - "Transfer" - ] - }, - { - "description": "The `add_bid` native entry point, used to create or top off a bid purse.", - "type": "string", - "enum": [ - "AddBid" - ] - }, - { - "description": "The `withdraw_bid` native entry point, used to decrease a stake.", - "type": "string", - "enum": [ - "WithdrawBid" - ] - }, - { - "description": "The `delegate` native entry point, used to add a new delegator or increase an existing delegator's stake.", - "type": "string", - "enum": [ - "Delegate" - ] - }, - { - "description": "The `undelegate` native entry point, used to reduce a delegator's stake or remove the delegator if the remaining stake is 0.", - "type": "string", - "enum": [ - "Undelegate" - ] - }, - { - "description": "The `redelegate` native entry point, used to reduce a delegator's stake or remove the delegator if the remaining stake is 0, and after the unbonding delay, automatically delegate to a new validator.", - "type": "string", - "enum": [ - "Redelegate" - ] - }, - { - "description": "The `activate_bid` native entry point, used to used to reactivate an inactive bid.", - "type": "string", - "enum": [ - "ActivateBid" - ] - }, - { - "description": "The `change_bid_public_key` native entry point, used to change a bid's public key.", - "type": "string", - "enum": [ - "ChangeBidPublicKey" - ] - }, - { - "description": "The `add_reservations` native entry point, used to add delegator to validator's reserve list", - "type": "string", - "enum": [ - "AddReservations" - ] - }, - { - "description": "The `cancel_reservations` native entry point, used to remove delegator from validator's reserve list", - "type": "string", - "enum": [ - "CancelReservations" - ] - } - ] - }, - "TransactionScheduling": { - "description": "Scheduling mode of a Transaction.", - "oneOf": [ - { - "description": "No special scheduling applied.", - "type": "string", - "enum": [ - "Standard" - ] - }, - { - "description": "Execution should be scheduled for the specified era.", - "type": "object", - "required": [ - "FutureEra" - ], - "properties": { - "FutureEra": { - "$ref": "#/definitions/EraId" - } - }, - "additionalProperties": false - }, - { - "description": "Execution should be scheduled for the specified timestamp or later.", - "type": "object", - "required": [ - "FutureTimestamp" - ], - "properties": { - "FutureTimestamp": { - "$ref": "#/definitions/Timestamp" - } - }, - "additionalProperties": false - } - ] - }, "ExecutionResult": { "description": "The versioned result of executing a single deploy.", "oneOf": [ @@ -4661,6 +4299,25 @@ } ] }, + "TransactionRuntime": { + "description": "Runtime used to execute a Transaction.", + "oneOf": [ + { + "description": "The Casper Version 1 Virtual Machine.", + "type": "string", + "enum": [ + "VmCasperV1" + ] + }, + { + "description": "The Casper Version 2 Virtual Machine.", + "type": "string", + "enum": [ + "VmCasperV2" + ] + } + ] + }, "ByteCodeHash": { "description": "The hash address of the contract wasm", "type": "string" diff --git a/types/src/block.rs b/types/src/block.rs index 703c8dbf05..58a8048c31 100644 --- a/types/src/block.rs +++ b/types/src/block.rs @@ -444,25 +444,44 @@ impl Block { Block::V1(_) => false, Block::V2(block_v2) => { let mint_count = block_v2.mint().count(); - if mint_count as u64 >= transaction_config.transaction_v1_config.native_mint_lane[4] + if mint_count as u64 + >= transaction_config + .transaction_v1_config + .native_mint_lane + .max_transaction_count() { return true; } let auction_count = block_v2.auction().count(); if auction_count as u64 - >= transaction_config.transaction_v1_config.native_auction_lane[4] + >= transaction_config + .transaction_v1_config + .native_auction_lane + .max_transaction_count() { return true; } - for (category, transactions) in block_v2.body.transactions() { + + let install_upgrade_count = block_v2.install_upgrade().count(); + if install_upgrade_count as u64 + >= transaction_config + .transaction_v1_config + .install_upgrade_lane + .max_transaction_count() + { + return true; + } + + for (lane_id, transactions) in block_v2.body.transactions() { let transaction_count = transactions.len(); - if *category < 2 { + if *lane_id < 2 { continue; }; let max_transaction_count = transaction_config .transaction_v1_config - .get_max_transaction_count(*category); + .get_max_transaction_count(*lane_id); + if transaction_count as u64 >= max_transaction_count { return true; } diff --git a/types/src/block/block_body/block_body_v2.rs b/types/src/block/block_body/block_body_v2.rs index 028c63ef8e..8aae7f75d9 100644 --- a/types/src/block/block_body/block_body_v2.rs +++ b/types/src/block/block_body/block_body_v2.rs @@ -11,8 +11,8 @@ use serde::{Deserialize, Serialize}; use crate::{ block::RewardedSignatures, bytesrepr::{self, FromBytes, ToBytes}, - transaction::TransactionCategory, - Digest, TransactionHash, + Digest, TransactionHash, AUCTION_LANE_ID, INSTALL_UPGRADE_LANE_ID, LARGE_WASM_LANE_ID, + MEDIUM_WASM_LANE_ID, MINT_LANE_ID, SMALL_WASM_LANE_ID, }; /// The body portion of a block. Version 2. @@ -47,50 +47,33 @@ impl BlockBodyV2 { } } - fn transaction_by_category( - &self, - transaction_category: TransactionCategory, - ) -> Vec { - match self.transactions.get(&(transaction_category as u8)) { + /// Returns the hashes of the transactions within the block filtered by lane_id. + pub fn transaction_by_lane(&self, lane_id: u8) -> impl Iterator { + match self.transactions.get(&lane_id) { Some(transactions) => transactions.to_vec(), None => vec![], } + .into_iter() } /// Returns the hashes of the mint transactions within the block. pub fn mint(&self) -> impl Iterator { - self.transaction_by_category(TransactionCategory::Mint) - .into_iter() + self.transaction_by_lane(MINT_LANE_ID) } /// Returns the hashes of the auction transactions within the block. pub fn auction(&self) -> impl Iterator { - self.transaction_by_category(TransactionCategory::Auction) - .into_iter() + self.transaction_by_lane(AUCTION_LANE_ID) } /// Returns the hashes of the installer/upgrader transactions within the block. pub fn install_upgrade(&self) -> impl Iterator { - self.transaction_by_category(TransactionCategory::InstallUpgrade) - .into_iter() + self.transaction_by_lane(INSTALL_UPGRADE_LANE_ID) } - /// Returns the hashes of all other transactions within the block. - pub fn large(&self) -> impl Iterator { - self.transaction_by_category(TransactionCategory::Large) - .into_iter() - } - - /// Returns the hashes of all other transactions within the block. - pub fn medium(&self) -> impl Iterator { - self.transaction_by_category(TransactionCategory::Medium) - .into_iter() - } - - /// Returns the hashes of all other transactions within the block. - pub fn small(&self) -> impl Iterator { - self.transaction_by_category(TransactionCategory::Small) - .into_iter() + /// Returns the hashes of the transactions filtered by lane id within the block. + pub fn transactions_by_lane_id(&self, lane_id: u8) -> impl Iterator { + self.transaction_by_lane(lane_id) } /// Returns a reference to the collection of mapped transactions. @@ -147,14 +130,13 @@ impl Display for BlockBodyV2 { fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { write!( formatter, - "block body, {} mint, {} auction, {} \ - installer/upgraders, {} large, {} medium, {} small", + "block body, {} mint, {} auction, {} install_upgrade, {} large wasm, {} medium wasm, {} small wasm", self.mint().count(), self.auction().count(), self.install_upgrade().count(), - self.large().count(), - self.medium().count(), - self.small().count() + self.transaction_by_lane(LARGE_WASM_LANE_ID).count(), + self.transaction_by_lane(MEDIUM_WASM_LANE_ID).count(), + self.transaction_by_lane(SMALL_WASM_LANE_ID).count(), ) } } diff --git a/types/src/block/block_v2.rs b/types/src/block/block_v2.rs index 8da4783c67..d20e2bedcf 100644 --- a/types/src/block/block_v2.rs +++ b/types/src/block/block_v2.rs @@ -25,7 +25,7 @@ use crate::{ PublicKey, Timestamp, TransactionHash, }; #[cfg(feature = "json-schema")] -use crate::{transaction::TransactionCategory, TransactionV1Hash}; +use crate::{TransactionV1Hash, AUCTION_LANE_ID, INSTALL_UPGRADE_LANE_ID, MINT_LANE_ID}; #[cfg(feature = "json-schema")] static BLOCK_V2: Lazy = Lazy::new(|| { @@ -49,18 +49,11 @@ static BLOCK_V2: Lazy = Lazy::new(|| { let installer_upgrader_hashes = vec![TransactionHash::V1(TransactionV1Hash::new( Digest::from([22; Digest::LENGTH]), ))]; - let standard = vec![TransactionHash::V1(TransactionV1Hash::new(Digest::from( - [23; Digest::LENGTH], - )))]; let transactions = { let mut ret = BTreeMap::new(); - ret.insert(TransactionCategory::Mint as u8, mint_hashes); - ret.insert(TransactionCategory::Auction as u8, auction_hashes); - ret.insert( - TransactionCategory::InstallUpgrade as u8, - installer_upgrader_hashes, - ); - ret.insert(TransactionCategory::Large as u8, standard); + ret.insert(MINT_LANE_ID, mint_hashes); + ret.insert(AUCTION_LANE_ID, auction_hashes); + ret.insert(INSTALL_UPGRADE_LANE_ID, installer_upgrader_hashes); ret }; let rewarded_signatures = RewardedSignatures::default(); @@ -254,24 +247,14 @@ impl BlockV2 { self.body.auction() } - /// Returns the hashes of the installer/upgrader transactions within the block. + /// Returns the hashes of the install/upgrade wasm transactions within the block. pub fn install_upgrade(&self) -> impl Iterator { self.body.install_upgrade() } - /// Returns the hashes of all other large transactions within the block. - pub fn large(&self) -> impl Iterator { - self.body.large() - } - - /// Returns the hashes of all other medium transactions within the block. - pub fn medium(&self) -> impl Iterator { - self.body.medium() - } - - /// Returns the hashes of all other small transactions within the block. - pub fn small(&self) -> impl Iterator { - self.body.small() + /// Returns the hashes of the transactions filtered by lane id within the block. + pub fn transactions_by_lane_id(&self, lane_id: u8) -> impl Iterator { + self.body.transaction_by_lane(lane_id) } /// Returns all of the transaction hashes in the order in which they were executed. diff --git a/types/src/block/test_block_builder/test_block_v2_builder.rs b/types/src/block/test_block_builder/test_block_v2_builder.rs index cc401aae80..d2c268a2f1 100644 --- a/types/src/block/test_block_builder/test_block_v2_builder.rs +++ b/types/src/block/test_block_builder/test_block_v2_builder.rs @@ -1,13 +1,13 @@ -use core::convert::TryInto; use std::iter; use alloc::collections::BTreeMap; use rand::Rng; use crate::{ - system::auction::ValidatorWeights, testing::TestRng, transaction::TransactionCategory, Block, - BlockHash, BlockV2, Digest, EraEndV2, EraId, ProtocolVersion, PublicKey, RewardedSignatures, - Timestamp, Transaction, U512, + system::auction::ValidatorWeights, testing::TestRng, Block, BlockHash, BlockV2, Digest, + EraEndV2, EraId, ProtocolVersion, PublicKey, RewardedSignatures, Timestamp, Transaction, + TransactionEntryPoint, TransactionTarget, AUCTION_LANE_ID, INSTALL_UPGRADE_LANE_ID, + LARGE_WASM_LANE_ID, MEDIUM_WASM_LANE_ID, MINT_LANE_ID, SMALL_WASM_LANE_ID, U512, }; /// A helper to build the blocks with various properties required for tests. @@ -180,30 +180,38 @@ impl TestBlockV2Builder { let mut small_hashes = vec![]; for txn in txns { let txn_hash = txn.hash(); - let category: TransactionCategory = txn - .transaction_category() - .try_into() - .expect("Expected a valid priority"); - match category { - TransactionCategory::Mint => mint_hashes.push(txn_hash), - TransactionCategory::Auction => auction_hashes.push(txn_hash), - TransactionCategory::InstallUpgrade => install_upgrade_hashes.push(txn_hash), - TransactionCategory::Large => large_hashes.push(txn_hash), - TransactionCategory::Medium => medium_hashes.push(txn_hash), - TransactionCategory::Small => small_hashes.push(txn_hash), + let lane_id = match txn { + Transaction::Deploy(deploy) => { + if deploy.is_transfer() { + MINT_LANE_ID + } else { + LARGE_WASM_LANE_ID + } + } + Transaction::V1(transaction_v1) => { + let entry_point = transaction_v1.get_transaction_entry_point().unwrap(); + let target = transaction_v1.get_transaction_target().unwrap(); + simplified_calculate_transaction_lane_from_values(&entry_point, &target) + } + }; + match lane_id { + MINT_LANE_ID => mint_hashes.push(txn_hash), + AUCTION_LANE_ID => auction_hashes.push(txn_hash), + INSTALL_UPGRADE_LANE_ID => install_upgrade_hashes.push(txn_hash), + LARGE_WASM_LANE_ID => large_hashes.push(txn_hash), + MEDIUM_WASM_LANE_ID => medium_hashes.push(txn_hash), + SMALL_WASM_LANE_ID => small_hashes.push(txn_hash), + _ => panic!("Invalid lane id"), } } let transactions = { let mut ret = BTreeMap::new(); - ret.insert(TransactionCategory::Mint as u8, mint_hashes); - ret.insert(TransactionCategory::Auction as u8, auction_hashes); - ret.insert( - TransactionCategory::InstallUpgrade as u8, - install_upgrade_hashes, - ); - ret.insert(TransactionCategory::Large as u8, large_hashes); - ret.insert(TransactionCategory::Medium as u8, medium_hashes); - ret.insert(TransactionCategory::Small as u8, small_hashes); + ret.insert(MINT_LANE_ID, mint_hashes); + ret.insert(AUCTION_LANE_ID, auction_hashes); + ret.insert(INSTALL_UPGRADE_LANE_ID, install_upgrade_hashes); + ret.insert(LARGE_WASM_LANE_ID, large_hashes); + ret.insert(MEDIUM_WASM_LANE_ID, medium_hashes); + ret.insert(SMALL_WASM_LANE_ID, small_hashes); ret }; let rewarded_signatures = rewarded_signatures.unwrap_or_default(); @@ -238,6 +246,72 @@ impl TestBlockV2Builder { } } +// A simplified way of calculating transaction lanes. It doesn't take +// into consideration the size of the transaction against the chainspec +// and doesn't take `additional_compufsdetation_factor` into consideration. +// This is only used for tests purposes. +fn simplified_calculate_transaction_lane_from_values( + entry_point: &TransactionEntryPoint, + target: &TransactionTarget, +) -> u8 { + match target { + TransactionTarget::Native => match entry_point { + TransactionEntryPoint::Transfer => MINT_LANE_ID, + TransactionEntryPoint::AddBid + | TransactionEntryPoint::WithdrawBid + | TransactionEntryPoint::Delegate + | TransactionEntryPoint::Undelegate + | TransactionEntryPoint::Redelegate + | TransactionEntryPoint::ActivateBid + | TransactionEntryPoint::ChangeBidPublicKey + | TransactionEntryPoint::AddReservations + | TransactionEntryPoint::CancelReservations => AUCTION_LANE_ID, + TransactionEntryPoint::Call => panic!("EntryPointCannotBeCall"), + TransactionEntryPoint::Custom(_) => panic!("EntryPointCannotBeCustom"), + }, + TransactionTarget::Stored { .. } => match entry_point { + TransactionEntryPoint::Custom(_) => LARGE_WASM_LANE_ID, + TransactionEntryPoint::Call + | TransactionEntryPoint::Transfer + | TransactionEntryPoint::AddBid + | TransactionEntryPoint::WithdrawBid + | TransactionEntryPoint::Delegate + | TransactionEntryPoint::Undelegate + | TransactionEntryPoint::Redelegate + | TransactionEntryPoint::ActivateBid + | TransactionEntryPoint::ChangeBidPublicKey + | TransactionEntryPoint::AddReservations + | TransactionEntryPoint::CancelReservations => { + panic!("EntryPointMustBeCustom") + } + }, + TransactionTarget::Session { + is_install_upgrade, .. + } => match entry_point { + TransactionEntryPoint::Call => { + if *is_install_upgrade { + INSTALL_UPGRADE_LANE_ID + } else { + LARGE_WASM_LANE_ID + } + } + TransactionEntryPoint::Custom(_) + | TransactionEntryPoint::Transfer + | TransactionEntryPoint::AddBid + | TransactionEntryPoint::WithdrawBid + | TransactionEntryPoint::Delegate + | TransactionEntryPoint::Undelegate + | TransactionEntryPoint::Redelegate + | TransactionEntryPoint::ActivateBid + | TransactionEntryPoint::ChangeBidPublicKey + | TransactionEntryPoint::AddReservations + | TransactionEntryPoint::CancelReservations => { + panic!("EntryPointMustBeCall") + } + }, + } +} + fn gen_era_end_v2( rng: &mut TestRng, validator_weights: Option>, diff --git a/types/src/chainspec.rs b/types/src/chainspec.rs index f20d4908eb..298b847635 100644 --- a/types/src/chainspec.rs +++ b/types/src/chainspec.rs @@ -62,11 +62,12 @@ pub use next_upgrade::NextUpgrade; pub use pricing_handling::PricingHandling; pub use protocol_config::ProtocolConfig; pub use refund_handling::RefundHandling; -pub use transaction_config::{DeployConfig, TransactionConfig, TransactionV1Config}; +pub use transaction_config::{ + DeployConfig, TransactionConfig, TransactionLimitsDefinition, TransactionV1Config, +}; #[cfg(any(feature = "testing", test))] pub use transaction_config::{ - DEFAULT_INSTALL_UPGRADE_GAS_LIMIT, DEFAULT_LARGE_TRANSACTION_GAS_LIMIT, - DEFAULT_MAX_PAYMENT_MOTES, DEFAULT_MIN_TRANSFER_MOTES, + DEFAULT_LARGE_TRANSACTION_GAS_LIMIT, DEFAULT_MAX_PAYMENT_MOTES, DEFAULT_MIN_TRANSFER_MOTES, }; pub use upgrade_config::ProtocolUpgradeConfig; pub use vacancy_config::VacancyConfig; @@ -222,39 +223,39 @@ impl Chainspec { .saturating_sub(self.core_config.gas_hold_interval.millis()) } - /// Is the given transaction category supported. - pub fn is_supported(&self, category: u8) -> bool { + /// Is the given transaction lane supported. + pub fn is_supported(&self, lane: u8) -> bool { self.transaction_config .transaction_v1_config - .is_supported(category) + .is_supported(lane) } /// Returns the max serialized for the given category. - pub fn get_max_serialized_length_by_category(&self, category: u8) -> u64 { + pub fn get_max_serialized_length_by_category(&self, lane: u8) -> u64 { self.transaction_config .transaction_v1_config - .get_max_serialized_length(category) + .get_max_serialized_length(lane) } /// Returns the max args length for the given category. - pub fn get_max_args_length_by_category(&self, category: u8) -> u64 { + pub fn get_max_args_length_by_category(&self, lane: u8) -> u64 { self.transaction_config .transaction_v1_config - .get_max_args_length(category) + .get_max_args_length(lane) } /// Returns the max gas limit for the given category. - pub fn get_max_gas_limit_by_category(&self, category: u8) -> u64 { + pub fn get_max_gas_limit_by_category(&self, lane: u8) -> u64 { self.transaction_config .transaction_v1_config - .get_max_gas_limit(category) + .get_max_transaction_gas_limit(lane) } /// Returns the max transaction count for the given category. - pub fn get_max_transaction_count_by_category(&self, category: u8) -> u64 { + pub fn get_max_transaction_count_by_category(&self, lane: u8) -> u64 { self.transaction_config .transaction_v1_config - .get_max_transaction_count(category) + .get_max_transaction_count(lane) } } diff --git a/types/src/chainspec/transaction_config.rs b/types/src/chainspec/transaction_config.rs index eef2f32f6f..7b2e584fd1 100644 --- a/types/src/chainspec/transaction_config.rs +++ b/types/src/chainspec/transaction_config.rs @@ -17,11 +17,9 @@ use crate::{ pub use deploy_config::DeployConfig; #[cfg(any(feature = "testing", test))] pub use deploy_config::DEFAULT_MAX_PAYMENT_MOTES; -pub use transaction_v1_config::TransactionV1Config; #[cfg(any(feature = "testing", test))] -pub use transaction_v1_config::{ - DEFAULT_INSTALL_UPGRADE_GAS_LIMIT, DEFAULT_LARGE_TRANSACTION_GAS_LIMIT, -}; +pub use transaction_v1_config::DEFAULT_LARGE_TRANSACTION_GAS_LIMIT; +pub use transaction_v1_config::{TransactionLimitsDefinition, TransactionV1Config}; /// The default minimum number of motes that can be transferred. pub const DEFAULT_MIN_TRANSFER_MOTES: u64 = 2_500_000_000; diff --git a/types/src/chainspec/transaction_config/transaction_v1_config.rs b/types/src/chainspec/transaction_config/transaction_v1_config.rs index 1523a61bad..8cbdca0656 100644 --- a/types/src/chainspec/transaction_config/transaction_v1_config.rs +++ b/types/src/chainspec/transaction_config/transaction_v1_config.rs @@ -1,53 +1,214 @@ +use core::cmp; + #[cfg(feature = "datasize")] use datasize::DataSize; +#[cfg(any(feature = "once_cell", test))] +use once_cell::sync::OnceCell; #[cfg(any(feature = "testing", test))] use rand::Rng; -use serde::{Deserialize, Serialize}; +use serde::{ + de::{Error, Unexpected}, + ser::SerializeSeq, + Deserialize, Deserializer, Serialize, Serializer, +}; #[cfg(any(feature = "testing", test))] use crate::testing::TestRng; -#[cfg(any(feature = "testing", test))] -use crate::INSTALL_UPGRADE_LANE_ID; use crate::{ bytesrepr::{self, FromBytes, ToBytes}, - transaction::TransactionCategory, + AUCTION_LANE_ID, INSTALL_UPGRADE_LANE_ID, MINT_LANE_ID, }; -/// Default gas limit of install / upgrade contracts -pub const DEFAULT_INSTALL_UPGRADE_GAS_LIMIT: u64 = 3_500_000_000_000; - /// Default gas limit of standard transactions pub const DEFAULT_LARGE_TRANSACTION_GAS_LIMIT: u64 = 500_000_000_000; const DEFAULT_NATIVE_MINT_LANE: [u64; 5] = [0, 1_048_576, 1024, 2_500_000_000, 650]; const DEFAULT_NATIVE_AUCTION_LANE: [u64; 5] = [1, 1_048_576, 1024, 2_500_000_000, 145]; +const DEFAULT_INSTALL_UPGRADE_LANE: [u64; 5] = [2, 1_048_576, 2048, 3_500_000_000_000, 2]; -const KIND: usize = 0; -const MAX_TRANSACTION_LENGTH: usize = 1; -const MAX_TRANSACTION_ARGS_LENGTH: usize = 2; -const MAX_TRANSACTION_GAS_LIMIT: usize = 3; -const MAX_TRANSACTION_COUNT: usize = 4; +const TRANSACTION_ID_INDEX: usize = 0; +const TRANSACTION_LENGTH_INDEX: usize = 1; +const TRANSACTION_ARGS_LENGTH_INDEX: usize = 2; +const TRANSACTION_GAS_LIMIT_INDEX: usize = 3; +const TRANSACTION_COUNT_INDEX: usize = 4; -/// Configuration values associated with V1 Transactions. +/// Structured limits imposed on a transaction lane #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] #[cfg_attr(feature = "datasize", derive(DataSize))] +pub struct TransactionLimitsDefinition { + /// The lane identifier + pub id: u8, + /// The maximum length of a transaction i bytes + pub max_transaction_length: u64, + /// The maximum number of runtime args + pub max_transaction_args_length: u64, + /// The maximum gas limit + pub max_transaction_gas_limit: u64, + /// The maximum number of transactions + pub max_transaction_count: u64, +} + +impl TryFrom> for TransactionLimitsDefinition { + type Error = TransactionConfigError; + + fn try_from(v: Vec) -> Result { + if v.len() != 5 { + return Err(TransactionConfigError::InvalidArgsProvided); + } + Ok(TransactionLimitsDefinition { + id: v[TRANSACTION_ID_INDEX] as u8, + max_transaction_length: v[TRANSACTION_LENGTH_INDEX], + max_transaction_args_length: v[TRANSACTION_ARGS_LENGTH_INDEX], + max_transaction_gas_limit: v[TRANSACTION_GAS_LIMIT_INDEX], + max_transaction_count: v[TRANSACTION_COUNT_INDEX], + }) + } +} + +impl TransactionLimitsDefinition { + /// Creates a new instance of TransactionLimitsDefinition + pub fn new( + id: u8, + max_transaction_length: u64, + max_transaction_args_length: u64, + max_transaction_gas_limit: u64, + max_transaction_count: u64, + ) -> Self { + Self { + id, + max_transaction_length, + max_transaction_args_length, + max_transaction_gas_limit, + max_transaction_count, + } + } + + fn as_vec(&self) -> Vec { + vec![ + self.id as u64, + self.max_transaction_length, + self.max_transaction_args_length, + self.max_transaction_gas_limit, + self.max_transaction_count, + ] + } + + /// Returns max_transaction_length + pub fn max_transaction_length(&self) -> u64 { + self.max_transaction_length + } + + /// Returns max_transaction_args_length + pub fn max_transaction_args_length(&self) -> u64 { + self.max_transaction_args_length + } + + /// Returns max_transaction_gas_limit + pub fn max_transaction_gas_limit(&self) -> u64 { + self.max_transaction_gas_limit + } + + /// Returns max_transaction_count + pub fn max_transaction_count(&self) -> u64 { + self.max_transaction_count + } + + /// Returns id + pub fn id(&self) -> u8 { + self.id + } +} + +#[derive(Debug, Clone)] +pub enum TransactionConfigError { + InvalidArgsProvided, +} + +/// Configuration values associated with V1 Transactions. +#[derive(Clone, Eq, Serialize, Deserialize, Debug)] +#[cfg_attr(feature = "datasize", derive(DataSize))] // Disallow unknown fields to ensure config files and command-line overrides contain valid keys. #[serde(deny_unknown_fields)] pub struct TransactionV1Config { + #[serde( + serialize_with = "limit_definition_to_vec", + deserialize_with = "vec_to_limit_definition" + )] /// Lane configuration of the native mint interaction. - pub native_mint_lane: Vec, + pub native_mint_lane: TransactionLimitsDefinition, + #[serde( + serialize_with = "limit_definition_to_vec", + deserialize_with = "vec_to_limit_definition" + )] /// Lane configuration for the native auction interaction. - pub native_auction_lane: Vec, - /// Lane configurations for the Wasm based lanes. - pub wasm_lanes: Vec>, + pub native_auction_lane: TransactionLimitsDefinition, + #[serde( + serialize_with = "limit_definition_to_vec", + deserialize_with = "vec_to_limit_definition" + )] + /// Lane configuration for the install/upgrade interaction. + pub install_upgrade_lane: TransactionLimitsDefinition, + #[serde( + serialize_with = "wasm_definitions_to_vec", + deserialize_with = "definition_to_wasms" + )] + /// Lane configurations for Wasm based lanes that are not declared as install/upgrade. + pub wasm_lanes: Vec, + #[cfg_attr(any(all(feature = "std", feature = "once_cell"), test), serde(skip))] + #[cfg_attr( + all(any(feature = "once_cell", test), feature = "datasize"), + data_size(skip) + )] + #[cfg(any(feature = "once_cell", test))] + wasm_lanes_ordered_by_transaction_size: OnceCell>, +} + +impl PartialEq for TransactionV1Config { + fn eq(&self, other: &TransactionV1Config) -> bool { + // Destructure to make sure we don't accidentally omit fields. + let TransactionV1Config { + native_mint_lane, + native_auction_lane, + install_upgrade_lane, + wasm_lanes, + #[cfg(any(feature = "once_cell", test))] + wasm_lanes_ordered_by_transaction_size: _, + } = self; + *native_mint_lane == other.native_mint_lane + && *native_auction_lane == other.native_auction_lane + && *install_upgrade_lane == other.install_upgrade_lane + && *wasm_lanes == other.wasm_lanes + } } impl TransactionV1Config { + /// Cretaes a new instance of TransactionV1Config + pub fn new( + native_mint_lane: TransactionLimitsDefinition, + native_auction_lane: TransactionLimitsDefinition, + install_upgrade_lane: TransactionLimitsDefinition, + wasm_lanes: Vec, + ) -> Self { + #[cfg(any(feature = "once_cell", test))] + let wasm_lanes_ordered_by_transaction_size = OnceCell::with_value( + Self::build_wasm_lanes_ordered_by_transaction_size(wasm_lanes.clone()), + ); + TransactionV1Config { + native_mint_lane, + native_auction_lane, + install_upgrade_lane, + wasm_lanes, + #[cfg(any(feature = "once_cell", test))] + wasm_lanes_ordered_by_transaction_size, + } + } + #[cfg(any(feature = "testing", test))] /// Generates a random instance using a `TestRng`. pub fn random(rng: &mut TestRng) -> Self { let native_mint_lane = DEFAULT_NATIVE_MINT_LANE.to_vec(); let native_auction_lane = DEFAULT_NATIVE_AUCTION_LANE.to_vec(); + let install_upgrade_lane = DEFAULT_INSTALL_UPGRADE_LANE.to_vec(); let mut wasm_lanes = vec![]; for kind in 2..7 { let lane = vec![ @@ -55,144 +216,93 @@ impl TransactionV1Config { rng.gen_range(0..=1_048_576), rng.gen_range(0..=1024), rng.gen_range(0..=2_500_000_000), + rng.gen_range(5..=150), ]; - wasm_lanes.push(lane) + wasm_lanes.push(lane.try_into().unwrap()) } - TransactionV1Config { - native_mint_lane, - native_auction_lane, + TransactionV1Config::new( + native_mint_lane.try_into().unwrap(), + native_auction_lane.try_into().unwrap(), + install_upgrade_lane.try_into().unwrap(), wasm_lanes, - } - } - - /// Returns true if the lane identifier is for either the mint or auction. - pub fn is_native_lane(&self, lane: u8) -> bool { - lane as u64 == DEFAULT_NATIVE_MINT_LANE[0] || lane as u64 == DEFAULT_NATIVE_AUCTION_LANE[0] + ) } - /// Returns the max serialized length of a transaction for the given category. - pub fn get_max_serialized_length(&self, category: u8) -> u64 { - if !self.is_supported(category) { - return 0; - } - match category { - 0 => self.native_mint_lane[MAX_TRANSACTION_LENGTH], - 1 => self.native_auction_lane[MAX_TRANSACTION_LENGTH], - _ => { - match self - .wasm_lanes - .iter() - .find(|lane| lane.first() == Some(&(category as u64))) - { - Some(wasm_lane) => wasm_lane[MAX_TRANSACTION_LENGTH], - None => 0, - } - } + /// Returns the max serialized length of a transaction for the given lane. + pub fn get_max_serialized_length(&self, lane_id: u8) -> u64 { + match lane_id { + MINT_LANE_ID => self.native_mint_lane.max_transaction_length, + AUCTION_LANE_ID => self.native_auction_lane.max_transaction_length, + INSTALL_UPGRADE_LANE_ID => self.install_upgrade_lane.max_transaction_length, + _ => match self.wasm_lanes.iter().find(|lane| lane.id == lane_id) { + Some(wasm_lane) => wasm_lane.max_transaction_length, + None => 0, + }, } } - /// Returns the max serialized args length of a transaction for the given category. - pub fn get_max_args_length(&self, category: u8) -> u64 { - if !self.is_supported(category) { - return 0; - } - match category { - 0 => self.native_mint_lane[MAX_TRANSACTION_ARGS_LENGTH], - 1 => self.native_auction_lane[MAX_TRANSACTION_ARGS_LENGTH], - _ => { - match self - .wasm_lanes - .iter() - .find(|lane| lane.first() == Some(&(category as u64))) - { - Some(wasm_lane) => wasm_lane[MAX_TRANSACTION_ARGS_LENGTH], - None => 0, - } - } + /// Returns the max number of runtime args + pub fn get_max_args_length(&self, lane_id: u8) -> u64 { + match lane_id { + MINT_LANE_ID => self.native_mint_lane.max_transaction_args_length, + AUCTION_LANE_ID => self.native_auction_lane.max_transaction_args_length, + INSTALL_UPGRADE_LANE_ID => self.install_upgrade_lane.max_transaction_args_length, + _ => match self.wasm_lanes.iter().find(|lane| lane.id == lane_id) { + Some(wasm_lane) => wasm_lane.max_transaction_args_length, + None => 0, + }, } } - /// Returns the max gas limit of a transaction for the given category. - pub fn get_max_gas_limit(&self, category: u8) -> u64 { - if !self.is_supported(category) { - return 0; - } - match category { - 0 => self.native_mint_lane[MAX_TRANSACTION_GAS_LIMIT], - 1 => self.native_auction_lane[MAX_TRANSACTION_GAS_LIMIT], - _ => { - match self - .wasm_lanes - .iter() - .find(|lane| lane.first() == Some(&(category as u64))) - { - Some(wasm_lane) => wasm_lane[MAX_TRANSACTION_GAS_LIMIT], - None => 0, - } - } + /// Returns the max gas limit of a transaction for the given lane. + pub fn get_max_transaction_gas_limit(&self, lane_id: u8) -> u64 { + match lane_id { + MINT_LANE_ID => self.native_mint_lane.max_transaction_gas_limit, + AUCTION_LANE_ID => self.native_auction_lane.max_transaction_gas_limit, + INSTALL_UPGRADE_LANE_ID => self.install_upgrade_lane.max_transaction_gas_limit, + _ => match self.wasm_lanes.iter().find(|lane| lane.id == lane_id) { + Some(wasm_lane) => wasm_lane.max_transaction_gas_limit, + None => 0, + }, } } - /// Returns the max gas limit of a transaction for the given category. - pub fn get_max_transaction_count(&self, category: u8) -> u64 { - if !self.is_supported(category) { - return 0; - } - match category { - 0 => self.native_mint_lane[MAX_TRANSACTION_COUNT], - 1 => self.native_auction_lane[MAX_TRANSACTION_COUNT], - _ => { - match self - .wasm_lanes - .iter() - .find(|lane| lane.first() == Some(&(category as u64))) - { - Some(wasm_lane) => wasm_lane[MAX_TRANSACTION_COUNT], - None => 0, - } - } - } - } - - /// Returns true if the given category is supported. - pub fn is_supported(&self, category: u8) -> bool { - if !self.is_native_lane(category) { - return self - .wasm_lanes - .iter() - .any(|lane| lane.first() == Some(&(category as u64))); + /// Returns the max transactions count for the given lane. + pub fn get_max_transaction_count(&self, lane_id: u8) -> u64 { + match lane_id { + MINT_LANE_ID => self.native_mint_lane.max_transaction_count, + AUCTION_LANE_ID => self.native_auction_lane.max_transaction_count, + INSTALL_UPGRADE_LANE_ID => self.install_upgrade_lane.max_transaction_count, + _ => match self.wasm_lanes.iter().find(|lane| lane.id == lane_id) { + Some(wasm_lane) => wasm_lane.max_transaction_count, + None => 0, + }, } - - true - } - - /// Returns the max total count for all transactions across all lanes allowed in a block. - pub fn get_max_block_count(&self) -> u64 { - self.native_mint_lane[MAX_TRANSACTION_COUNT] - + self.native_auction_lane[MAX_TRANSACTION_COUNT] - + self - .wasm_lanes - .iter() - .map(|lane| lane[MAX_TRANSACTION_COUNT]) - .sum::() } /// Returns the maximum number of Wasm based transactions across wasm lanes. pub fn get_max_wasm_transaction_count(&self) -> u64 { let mut ret = 0; for lane in self.wasm_lanes.iter() { - ret += lane[MAX_TRANSACTION_COUNT]; + ret += lane.max_transaction_count; } ret } + /// Are the given transaction parameters supported. + pub fn is_supported(&self, lane_id: u8) -> bool { + if !self.is_predefined_lane(lane_id) { + return self.wasm_lanes.iter().any(|lane| lane.id == lane_id); + } + true + } + /// Returns the list of currently supported lane identifiers. - pub fn get_supported_categories(&self) -> Vec { - let mut ret = vec![0, 1]; + pub fn get_supported_lanes(&self) -> Vec { + let mut ret = vec![0, 1, 2]; for lane in self.wasm_lanes.iter() { - let lane_id = lane[KIND] as u8; - ret.push(lane_id); + ret.push(lane.id); } ret } @@ -207,75 +317,126 @@ impl TransactionV1Config { large: Option, ) -> Self { if let Some(mint_count) = mint { - self.native_mint_lane[MAX_TRANSACTION_COUNT] = mint_count; + self.native_mint_lane.max_transaction_count = mint_count; } if let Some(auction_count) = auction { - self.native_auction_lane[MAX_TRANSACTION_COUNT] = auction_count; + self.native_auction_lane.max_transaction_count = auction_count; } - if let Some(install_upgrade_count) = install { - let (index, lane) = self - .wasm_lanes - .iter() - .enumerate() - .find(|(_, lane)| lane.first() == Some(&(INSTALL_UPGRADE_LANE_ID as u64))) - .expect("must get install upgrade lane"); - let mut updated_lane = lane.clone(); - self.wasm_lanes.remove(index); - updated_lane[MAX_TRANSACTION_COUNT] = install_upgrade_count; - self.wasm_lanes.push(updated_lane); + if let Some(install_upgrade) = install { + self.install_upgrade_lane.max_transaction_count = install_upgrade; } if let Some(large_limit) = large { - let (index, lane) = self + for lane in self.wasm_lanes.iter_mut() { + if lane.id == 3 { + lane.max_transaction_count = large_limit; + } + } + } + self + } + + /// Returns the max total count for all transactions across all lanes allowed in a block. + pub fn get_max_block_count(&self) -> u64 { + self.native_mint_lane.max_transaction_count + + self.native_auction_lane.max_transaction_count + + self.install_upgrade_lane.max_transaction_count + + self .wasm_lanes .iter() - .enumerate() - .find(|(_, lane)| lane.first() == Some(&3)) - .expect("must get install upgrade lane"); - let mut updated_lane = lane.clone(); - self.wasm_lanes.remove(index); - updated_lane[MAX_TRANSACTION_COUNT] = large_limit; - self.wasm_lanes.push(updated_lane); + .map(|lane| lane.max_transaction_count) + .sum::() + } + + /// Returns true if the lane identifier is for one of the predefined lanes. + pub fn is_predefined_lane(&self, lane: u8) -> bool { + lane == AUCTION_LANE_ID || lane == MINT_LANE_ID || lane == INSTALL_UPGRADE_LANE_ID + } + + /// Returns a wasm lane id based on the transaction size adjusted by + /// maybe_additional_computation_factor if necessary. + pub fn get_wasm_lane_id( + &self, + transaction_size: u64, + additional_computation_factor: u8, + ) -> Option { + let mut maybe_adequate_lane_index = None; + let buckets = self.get_wasm_lanes_ordered(); + let number_of_lanes = buckets.len(); + for (i, lane) in buckets.iter().enumerate() { + let lane_size = lane.max_transaction_length; + if lane_size >= transaction_size { + maybe_adequate_lane_index = Some(i); + break; + } } - self + if let Some(adequate_lane_index) = maybe_adequate_lane_index { + maybe_adequate_lane_index = Some(cmp::min( + adequate_lane_index + additional_computation_factor as usize, + number_of_lanes - 1, + )); + } + maybe_adequate_lane_index.map(|index| buckets[index].id) + } + + #[allow(unreachable_code)] + //We're allowing unreachable code here because there's a possibility that someone might + // want to use the types crate without once_cell + fn get_wasm_lanes_ordered(&self) -> &Vec { + #[cfg(any(feature = "once_cell", test))] + return self.wasm_lanes_ordered_by_transaction_size.get_or_init(|| { + Self::build_wasm_lanes_ordered_by_transaction_size(self.wasm_lanes.clone()) + }); + &Self::build_wasm_lanes_ordered_by_transaction_size(self.wasm_lanes.clone()) + } + + fn build_wasm_lanes_ordered_by_transaction_size( + wasm_lanes: Vec, + ) -> Vec { + let mut wasm_lanes_ordered_by_transaction_size = wasm_lanes; + wasm_lanes_ordered_by_transaction_size + .sort_by(|a, b| a.max_transaction_length.cmp(&b.max_transaction_length)); + wasm_lanes_ordered_by_transaction_size } } #[cfg(any(feature = "std", test))] impl Default for TransactionV1Config { fn default() -> Self { - let large_lane = vec![ - TransactionCategory::Large as u64, + let wasm_lane = vec![ + 3_u64, //large lane id 1_048_576, 1024, DEFAULT_LARGE_TRANSACTION_GAS_LIMIT, 10, ]; - let install_upgrade_lane = vec![ - TransactionCategory::InstallUpgrade as u64, - 1_048_576, - 2048, - DEFAULT_INSTALL_UPGRADE_GAS_LIMIT, - 2, - ]; - let native_mint_lane = DEFAULT_NATIVE_MINT_LANE.to_vec(); let native_auction_lane = DEFAULT_NATIVE_AUCTION_LANE.to_vec(); - let wasm_lanes = vec![large_lane, install_upgrade_lane]; + let install_upgrade_lane = DEFAULT_INSTALL_UPGRADE_LANE.to_vec(); + let raw_wasm_lanes = vec![wasm_lane]; + let wasm_lanes: Result, _> = + raw_wasm_lanes.into_iter().map(|v| v.try_into()).collect(); - TransactionV1Config { - native_mint_lane, - native_auction_lane, - wasm_lanes, - } + TransactionV1Config::new( + native_mint_lane.try_into().unwrap(), + native_auction_lane.try_into().unwrap(), + install_upgrade_lane.try_into().unwrap(), + wasm_lanes.unwrap(), + ) } } impl ToBytes for TransactionV1Config { fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { - self.native_mint_lane.write_bytes(writer)?; - self.native_auction_lane.write_bytes(writer)?; - self.wasm_lanes.write_bytes(writer) + self.native_mint_lane.as_vec().write_bytes(writer)?; + self.native_auction_lane.as_vec().write_bytes(writer)?; + self.install_upgrade_lane.as_vec().write_bytes(writer)?; + let wasm_lanes_as_vecs: Vec> = self + .wasm_lanes + .iter() + .map(TransactionLimitsDefinition::as_vec) + .collect(); + wasm_lanes_as_vecs.write_bytes(writer) } fn to_bytes(&self) -> Result, bytesrepr::Error> { @@ -285,30 +446,119 @@ impl ToBytes for TransactionV1Config { } fn serialized_length(&self) -> usize { - self.native_mint_lane.serialized_length() - + self.native_auction_lane.serialized_length() - + self.wasm_lanes.serialized_length() + let wasm_lanes_as_vecs: Vec> = self + .wasm_lanes + .iter() + .map(TransactionLimitsDefinition::as_vec) + .collect(); + self.native_mint_lane.as_vec().serialized_length() + + self.native_auction_lane.as_vec().serialized_length() + + self.install_upgrade_lane.as_vec().serialized_length() + + wasm_lanes_as_vecs.serialized_length() } } impl FromBytes for TransactionV1Config { fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { - let (native_mint_lane, remainder) = FromBytes::from_bytes(bytes)?; - let (native_auction_lane, remainder) = FromBytes::from_bytes(remainder)?; - let (wasm_lanes, remainder) = FromBytes::from_bytes(remainder)?; - let config = TransactionV1Config { + let (raw_native_mint_lane, remainder): (Vec, &[u8]) = FromBytes::from_bytes(bytes)?; + let (raw_native_auction_lane, remainder): (Vec, &[u8]) = + FromBytes::from_bytes(remainder)?; + let (raw_install_upgrade_lane, remainder): (Vec, &[u8]) = + FromBytes::from_bytes(remainder)?; + let (raw_wasm_lanes, remainder): (Vec>, &[u8]) = FromBytes::from_bytes(remainder)?; + let native_mint_lane = raw_native_mint_lane + .try_into() + .map_err(|_| bytesrepr::Error::Formatting)?; + let native_auction_lane = raw_native_auction_lane + .try_into() + .map_err(|_| bytesrepr::Error::Formatting)?; + let install_upgrade_lane = raw_install_upgrade_lane + .try_into() + .map_err(|_| bytesrepr::Error::Formatting)?; + let wasm_lanes: Result, _> = + raw_wasm_lanes.into_iter().map(|v| v.try_into()).collect(); + let config = TransactionV1Config::new( native_mint_lane, native_auction_lane, - wasm_lanes, - }; + install_upgrade_lane, + wasm_lanes.map_err(|_| bytesrepr::Error::Formatting)?, + ); Ok((config, remainder)) } } +fn vec_to_limit_definition<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let vec = Vec::::deserialize(deserializer)?; + let limits = TransactionLimitsDefinition::try_from(vec).map_err(|_| { + D::Error::invalid_value( + Unexpected::Seq, + &"expected 5 u64 compliant numbers to create a TransactionLimitsDefinition", + ) + })?; + Ok(limits) +} + +fn limit_definition_to_vec( + limits: &TransactionLimitsDefinition, + serializer: S, +) -> Result +where + S: Serializer, +{ + let vec = limits.as_vec(); + let mut seq = serializer.serialize_seq(Some(vec.len()))?; + for element in vec { + seq.serialize_element(&element)?; + } + seq.end() +} + +fn definition_to_wasms<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let vec = Vec::>::deserialize(deserializer)?; + let result: Result, TransactionConfigError> = + vec.into_iter().map(|v| v.try_into()).collect(); + result.map_err(|_| { + D::Error::invalid_value( + Unexpected::Seq, + &"sequence of sequences to assemble wasm definitions", + ) + }) +} + +fn wasm_definitions_to_vec( + limits: &[TransactionLimitsDefinition], + serializer: S, +) -> Result +where + S: Serializer, +{ + let vec_of_vecs: Vec> = limits.iter().map(|v| v.as_vec()).collect(); + let mut seq = serializer.serialize_seq(Some(vec_of_vecs.len()))?; + for element in vec_of_vecs { + seq.serialize_element(&element)?; + } + seq.end() +} + #[cfg(test)] mod tests { - use super::*; + use serde_json::Value; + use super::*; + const EXAMPLE_JSON: &str = r#"{ + "native_mint_lane": [0,1,2,3,4], + "native_auction_lane": [1,5,6,7,8], + "install_upgrade_lane": [2,9,10,11,12], + "wasm_lanes": [[3,13,14,15,16], [4,17,18,19,20], [5,21,22,23,24]] + }"#; #[test] fn bytesrepr_roundtrip() { let mut rng = TestRng::new(); @@ -321,7 +571,183 @@ mod tests { let config = TransactionV1Config::default(); assert!(config.is_supported(0)); assert!(config.is_supported(1)); + assert!(config.is_supported(2)); assert!(config.is_supported(3)); assert!(!config.is_supported(10)); } + + #[test] + fn should_get_configuration_for_wasm() { + let config = build_example_transaction_config(); + let got = config.get_wasm_lane_id(100, 0); + assert_eq!(got, Some(3)); + let config = build_example_transaction_config_reverse_wasm_ids(); + let got = config.get_wasm_lane_id(100, 0); + assert_eq!(got, Some(5)); + } + + #[test] + fn given_too_big_transaction_should_return_none() { + let config = build_example_transaction_config(); + let got = config.get_wasm_lane_id(100000000, 0); + assert!(got.is_none()); + let config = build_example_transaction_config_reverse_wasm_ids(); + let got = config.get_wasm_lane_id(100000000, 0); + assert!(got.is_none()); + } + + #[test] + fn given_wasm_should_return_first_fit() { + let config = build_example_transaction_config(); + + let got = config.get_wasm_lane_id(660, 0); + assert_eq!(got, Some(4)); + + let got = config.get_wasm_lane_id(800, 0); + assert_eq!(got, Some(5)); + + let got = config.get_wasm_lane_id(1, 0); + assert_eq!(got, Some(3)); + + let config = build_example_transaction_config_reverse_wasm_ids(); + + let got = config.get_wasm_lane_id(660, 0); + assert_eq!(got, Some(4)); + + let got = config.get_wasm_lane_id(800, 0); + assert_eq!(got, Some(3)); + + let got = config.get_wasm_lane_id(1, 0); + assert_eq!(got, Some(5)); + } + + #[test] + fn given_additional_computation_factor_should_be_applied() { + let config = build_example_transaction_config(); + let got = config.get_wasm_lane_id(660, 1); + assert_eq!(got, Some(5)); + + let config = build_example_transaction_config_reverse_wasm_ids(); + let got = config.get_wasm_lane_id(660, 1); + assert_eq!(got, Some(3)); + } + + #[test] + fn given_additional_computation_factor_should_not_overflow() { + let config = build_example_transaction_config(); + let got = config.get_wasm_lane_id(660, 2); + assert_eq!(got, Some(5)); + let got_2 = config.get_wasm_lane_id(660, 20); + assert_eq!(got_2, Some(5)); + + let config = build_example_transaction_config_reverse_wasm_ids(); + let got = config.get_wasm_lane_id(660, 2); + assert_eq!(got, Some(3)); + let got_2 = config.get_wasm_lane_id(660, 20); + assert_eq!(got_2, Some(3)); + } + + #[test] + fn given_no_wasm_lanes_should_return_none() { + let config = build_example_transaction_config_no_wasms(); + let got = config.get_wasm_lane_id(660, 2); + assert!(got.is_none()); + let got = config.get_wasm_lane_id(660, 0); + assert!(got.is_none()); + let got = config.get_wasm_lane_id(660, 20); + assert!(got.is_none()); + } + + #[test] + fn should_deserialize() { + let got: TransactionV1Config = serde_json::from_str(EXAMPLE_JSON).unwrap(); + let expected = TransactionV1Config::new( + TransactionLimitsDefinition::new(0, 1, 2, 3, 4), + TransactionLimitsDefinition::new(1, 5, 6, 7, 8), + TransactionLimitsDefinition::new(2, 9, 10, 11, 12), + vec![ + TransactionLimitsDefinition::new(3, 13, 14, 15, 16), + TransactionLimitsDefinition::new(4, 17, 18, 19, 20), + TransactionLimitsDefinition::new(5, 21, 22, 23, 24), + ], + ); + assert_eq!(got, expected); + } + + #[test] + fn should_serialize() { + let input = TransactionV1Config::new( + TransactionLimitsDefinition::new(0, 1, 2, 3, 4), + TransactionLimitsDefinition::new(1, 5, 6, 7, 8), + TransactionLimitsDefinition::new(2, 9, 10, 11, 12), + vec![ + TransactionLimitsDefinition::new(3, 13, 14, 15, 16), + TransactionLimitsDefinition::new(4, 17, 18, 19, 20), + TransactionLimitsDefinition::new(5, 21, 22, 23, 24), + ], + ); + let raw = serde_json::to_string(&input).unwrap(); + let got = serde_json::from_str::(&raw).unwrap(); + let expected: Value = serde_json::from_str::(EXAMPLE_JSON).unwrap(); + assert_eq!(got, expected); + } + + fn example_native() -> TransactionLimitsDefinition { + TransactionLimitsDefinition::new(0, 1500, 1024, 1_500_000_000, 150) + } + + fn example_auction() -> TransactionLimitsDefinition { + TransactionLimitsDefinition::new(1, 500, 3024, 3_500_000_000, 350) + } + + fn example_install_upgrade() -> TransactionLimitsDefinition { + TransactionLimitsDefinition::new(2, 10000, 2024, 2_500_000_000, 250) + } + + fn wasm_small(id: u8) -> TransactionLimitsDefinition { + TransactionLimitsDefinition::new(id, 600, 4024, 4_500_000_000, 450) + } + + fn wasm_medium(id: u8) -> TransactionLimitsDefinition { + TransactionLimitsDefinition::new(id, 700, 5024, 5_500_000_000, 550) + } + + fn wasm_large(id: u8) -> TransactionLimitsDefinition { + TransactionLimitsDefinition::new(id, 800, 6024, 6_500_000_000, 650) + } + + fn example_wasm() -> Vec { + vec![wasm_small(3), wasm_medium(4), wasm_large(5)] + } + + fn example_wasm_reversed_ids() -> Vec { + vec![wasm_small(5), wasm_medium(4), wasm_large(3)] + } + + fn build_example_transaction_config_no_wasms() -> TransactionV1Config { + TransactionV1Config::new( + example_native(), + example_auction(), + example_install_upgrade(), + vec![], + ) + } + + fn build_example_transaction_config() -> TransactionV1Config { + TransactionV1Config::new( + example_native(), + example_auction(), + example_install_upgrade(), + example_wasm(), + ) + } + + fn build_example_transaction_config_reverse_wasm_ids() -> TransactionV1Config { + TransactionV1Config::new( + example_native(), + example_auction(), + example_install_upgrade(), + example_wasm_reversed_ids(), + ) + } } diff --git a/types/src/deploy_info.rs b/types/src/deploy_info.rs index a741cf96fd..6ba9436580 100644 --- a/types/src/deploy_info.rs +++ b/types/src/deploy_info.rs @@ -107,14 +107,13 @@ impl ToBytes for DeployInfo { /// Generators for a `DeployInfo` #[cfg(any(feature = "testing", feature = "gens", test))] pub(crate) mod gens { - use proptest::{collection, prelude::Strategy}; - use crate::{ gens::{account_hash_arb, u512_arb, uref_arb}, transaction::gens::deploy_hash_arb, transfer::gens::transfer_v1_addr_arb, DeployInfo, }; + use proptest::{collection, prelude::Strategy}; pub fn deploy_info_arb() -> impl Strategy { let transfers_length_range = 0..5; diff --git a/types/src/gens.rs b/types/src/gens.rs index d8f7b63897..1d109f7017 100644 --- a/types/src/gens.rs +++ b/types/src/gens.rs @@ -9,14 +9,6 @@ use alloc::{ vec, }; -use proptest::{ - array, bits, bool, - collection::{self, vec, SizeRange}, - option, - prelude::*, - result, -}; - use crate::{ account::{ self, action_thresholds::gens::account_action_thresholds_arb, @@ -34,7 +26,10 @@ use crate::{ Contract, ContractHash, ContractPackage, ContractPackageStatus, ContractVersionKey, ContractVersions, EntryPoint as ContractEntryPoint, EntryPoints as ContractEntryPoints, }, - crypto::{self, gens::public_key_arb_no_system}, + crypto::{ + self, + gens::{public_key_arb_no_system, secret_key_arb_no_system}, + }, deploy_info::gens::deploy_info_arb, global_state::{Pointer, TrieMerkleProof, TrieMerkleProofStep}, package::{EntityVersionKey, EntityVersions, Groups, PackageStatus}, @@ -47,7 +42,9 @@ use crate::{ mint::BalanceHoldAddr, SystemEntityType, }, - transaction::gens::deploy_hash_arb, + transaction::{ + gens::deploy_hash_arb, FieldsContainer, InitiatorAddrAndSecretKey, TransactionV1Payload, + }, transfer::{ gens::{transfer_v1_addr_arb, transfer_v1_arb}, TransferAddr, @@ -55,9 +52,16 @@ use crate::{ AccessRights, AddressableEntity, AddressableEntityHash, BlockTime, ByteCode, CLType, CLValue, Digest, EntityAddr, EntityKind, EntryPoint, EntryPointAccess, EntryPointPayment, EntryPointType, EntryPoints, EraId, Group, InitiatorAddr, Key, NamedArg, Package, Parameter, - Phase, PricingMode, ProtocolVersion, RuntimeArgs, SemVer, StoredValue, Timestamp, - TransactionCategory, TransactionEntryPoint, TransactionInvocationTarget, TransactionRuntime, - TransactionScheduling, TransactionTarget, TransactionV1Body, URef, U128, U256, U512, + Phase, PricingMode, ProtocolVersion, PublicKey, RuntimeArgs, SemVer, StoredValue, TimeDiff, + Timestamp, Transaction, TransactionEntryPoint, TransactionInvocationTarget, TransactionRuntime, + TransactionScheduling, TransactionTarget, TransactionV1, URef, U128, U256, U512, +}; +use proptest::{ + array, bits, bool, + collection::{self, vec, SizeRange}, + option, + prelude::*, + result, }; pub fn u8_slice_32() -> impl Strategy { @@ -919,17 +923,6 @@ pub fn trie_merkle_proof_arb() -> impl Strategy impl Strategy { - prop_oneof![ - Just(TransactionCategory::Mint), - Just(TransactionCategory::Auction), - Just(TransactionCategory::InstallUpgrade), - Just(TransactionCategory::Large), - Just(TransactionCategory::Medium), - Just(TransactionCategory::Small), - ] -} - pub fn transaction_scheduling_arb() -> impl Strategy { prop_oneof![ Just(TransactionScheduling::Standard), @@ -961,6 +954,25 @@ pub fn transaction_invocation_target_arb() -> impl Strategy impl Strategy { + ( + transaction_invocation_target_arb(), + transaction_runtime_arb(), + ) + .prop_map(|(target, runtime)| TransactionTarget::new_stored(target, runtime)) +} + +pub fn session_transaction_target() -> impl Strategy { + ( + any::(), + Just(Bytes::from(vec![1; 10])), + transaction_runtime_arb(), + ) + .prop_map(|(target, module_bytes, runtime)| { + TransactionTarget::new_session(target, module_bytes, runtime) + }) +} + pub fn transaction_target_arb() -> impl Strategy { prop_oneof![ Just(TransactionTarget::Native), @@ -971,9 +983,19 @@ pub fn transaction_target_arb() -> impl Strategy { .prop_map(|(target, runtime)| TransactionTarget::new_stored(target, runtime)) ] } -pub fn transaction_entry_point_arb() -> impl Strategy { + +pub fn legal_target_entry_point_calls_arb( +) -> impl Strategy { + prop_oneof![ + native_entry_point_arb().prop_map(|s| (TransactionTarget::Native, s)), + stored_transaction_target() + .prop_map(|s| (s, TransactionEntryPoint::Custom("ABC".to_string()))), + session_transaction_target().prop_map(|s| (s, TransactionEntryPoint::Call)), + ] +} + +pub fn native_entry_point_arb() -> impl Strategy { prop_oneof![ - Just(TransactionEntryPoint::Call), Just(TransactionEntryPoint::Transfer), Just(TransactionEntryPoint::AddBid), Just(TransactionEntryPoint::WithdrawBid), @@ -982,11 +1004,17 @@ pub fn transaction_entry_point_arb() -> impl Strategy impl Strategy { + prop_oneof![ + native_entry_point_arb(), + Just(TransactionEntryPoint::Call), + Just(TransactionEntryPoint::Custom("custom".to_string())), + ] +} pub fn runtime_args_arb() -> impl Strategy { let mut runtime_args_1 = RuntimeArgs::new(); @@ -1001,27 +1029,45 @@ pub fn runtime_args_arb() -> impl Strategy { prop_oneof![Just(runtime_args_1)] } -pub fn v1_transaction_body_arb() -> impl Strategy { +pub fn fields_arb() -> impl Strategy> { + collection::btree_map( + any::(), + any::().prop_map(|s| Bytes::from(s.as_bytes())), + 3..30, + ) +} +pub fn v1_transaction_payload_arb() -> impl Strategy { ( - runtime_args_arb(), - transaction_target_arb(), - transaction_entry_point_arb(), - transaction_category_arb(), - transaction_scheduling_arb(), + any::(), + timestamp_arb(), + any::(), + pricing_mode_arb(), + initiator_addr_arb(), + fields_arb(), ) .prop_map( - |(args, target, entry_point, transaction_category, scheduling)| { - TransactionV1Body::new( - args, - target, - entry_point, - transaction_category as u8, - scheduling, + |(chain_name, timestamp, ttl_millis, pricing_mode, initiator_addr, fields)| { + TransactionV1Payload::new( + chain_name, + timestamp, + TimeDiff::from_millis(ttl_millis), + pricing_mode, + initiator_addr, + fields, ) }, ) } +pub fn fixed_pricing_mode_arb() -> impl Strategy { + (any::(), any::()).prop_map(|(gas_price_tolerance, additional_computation_factor)| { + PricingMode::Fixed { + gas_price_tolerance, + additional_computation_factor, + } + }) +} + pub fn pricing_mode_arb() -> impl Strategy { prop_oneof![ (any::(), any::(), any::()).prop_map( @@ -1033,11 +1079,7 @@ pub fn pricing_mode_arb() -> impl Strategy { } } ), - any::().prop_map(|gas_price_tolerance| { - PricingMode::Fixed { - gas_price_tolerance, - } - }), + fixed_pricing_mode_arb(), u8_slice_32().prop_map(|receipt| { PricingMode::Reserved { receipt: receipt.into(), @@ -1052,3 +1094,100 @@ pub fn initiator_addr_arb() -> impl Strategy { u2_slice_32().prop_map(|hash| InitiatorAddr::AccountHash(AccountHash::new(hash))), ] } + +pub fn timestamp_arb() -> impl Strategy { + //The weird u64 value is the max milliseconds that are bofeore year 10000. 5 digit years are + // not rfc3339 compliant and will cause an error + prop_oneof![Just(0_u64), Just(1_u64), Just(253_402_300_799_999_u64)].prop_map(Timestamp::from) +} + +pub fn legal_v1_transaction_arb() -> impl Strategy { + ( + any::(), + timestamp_arb(), + any::(), + pricing_mode_arb(), + secret_key_arb_no_system(), + runtime_args_arb(), + transaction_scheduling_arb(), + legal_target_entry_point_calls_arb(), + ) + .prop_map( + |( + chain_name, + timestamp, + ttl, + pricing_mode, + secret_key, + args, + scheduling, + (target, entry_point), + )| { + let public_key = PublicKey::from(&secret_key); + let initiator_addr = InitiatorAddr::PublicKey(public_key); + let initiator_addr_with_secret = InitiatorAddrAndSecretKey::Both { + initiator_addr, + secret_key: &secret_key, + }; + let container = FieldsContainer::new(args, target, entry_point, scheduling); + TransactionV1::build( + chain_name, + timestamp, + TimeDiff::from_seconds(ttl), + pricing_mode, + container.to_map().unwrap(), + initiator_addr_with_secret, + ) + }, + ) +} +pub fn v1_transaction_arb() -> impl Strategy { + ( + any::(), + timestamp_arb(), + any::(), + pricing_mode_arb(), + secret_key_arb_no_system(), + runtime_args_arb(), + transaction_target_arb(), + transaction_entry_point_arb(), + transaction_scheduling_arb(), + ) + .prop_map( + |( + chain_name, + timestamp, + ttl, + pricing_mode, + secret_key, + args, + target, + entry_point, + scheduling, + )| { + let public_key = PublicKey::from(&secret_key); + let initiator_addr = InitiatorAddr::PublicKey(public_key); + let initiator_addr_with_secret = InitiatorAddrAndSecretKey::Both { + initiator_addr, + secret_key: &secret_key, + }; + let container = FieldsContainer::new(args, target, entry_point, scheduling); + TransactionV1::build( + chain_name, + timestamp, + TimeDiff::from_seconds(ttl), + pricing_mode, + container.to_map().unwrap(), + initiator_addr_with_secret, + ) + }, + ) +} + +pub fn transaction_arb() -> impl Strategy { + (v1_transaction_arb()).prop_map(Transaction::V1) +} + +pub fn legal_transaction_arb() -> impl Strategy { + (legal_v1_transaction_arb()).prop_map(Transaction::V1) +} diff --git a/types/src/lib.rs b/types/src/lib.rs index 2607e9f45b..2c93f1f5a2 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -127,9 +127,9 @@ pub use chainspec::{ HoldBalanceHandling, HostFunction, HostFunctionCost, HostFunctionCosts, LegacyRequiredFinality, MessageLimits, MintCosts, NetworkConfig, NextUpgrade, OpcodeCosts, PricingHandling, ProtocolConfig, ProtocolUpgradeConfig, RefundHandling, StandardPaymentCosts, StorageCosts, - SystemConfig, TransactionConfig, TransactionV1Config, VacancyConfig, ValidatorConfig, - WasmConfig, DEFAULT_GAS_HOLD_INTERVAL, DEFAULT_HOST_FUNCTION_NEW_DICTIONARY, - DEFAULT_REFUND_HANDLING, + SystemConfig, TransactionConfig, TransactionLimitsDefinition, TransactionV1Config, + VacancyConfig, ValidatorConfig, WasmConfig, DEFAULT_GAS_HOLD_INTERVAL, + DEFAULT_HOST_FUNCTION_NEW_DICTIONARY, DEFAULT_REFUND_HANDLING, }; #[cfg(any(all(feature = "std", feature = "testing"), test))] pub use chainspec::{ @@ -143,11 +143,10 @@ pub use chainspec::{ DEFAULT_CONTROL_FLOW_RETURN_OPCODE, DEFAULT_CONTROL_FLOW_SELECT_OPCODE, DEFAULT_CONVERSION_COST, DEFAULT_CURRENT_MEMORY_COST, DEFAULT_DELEGATE_COST, DEFAULT_DIV_COST, DEFAULT_FEE_HANDLING, DEFAULT_GAS_HOLD_BALANCE_HANDLING, DEFAULT_GLOBAL_COST, - DEFAULT_GROW_MEMORY_COST, DEFAULT_INSTALL_UPGRADE_GAS_LIMIT, DEFAULT_INTEGER_COMPARISON_COST, - DEFAULT_LARGE_TRANSACTION_GAS_LIMIT, DEFAULT_LOAD_COST, DEFAULT_LOCAL_COST, - DEFAULT_MAX_PAYMENT_MOTES, DEFAULT_MAX_STACK_HEIGHT, DEFAULT_MIN_TRANSFER_MOTES, - DEFAULT_MUL_COST, DEFAULT_NEW_DICTIONARY_COST, DEFAULT_NOP_COST, DEFAULT_STORE_COST, - DEFAULT_TRANSFER_COST, DEFAULT_UNREACHABLE_COST, DEFAULT_WASM_MAX_MEMORY, + DEFAULT_GROW_MEMORY_COST, DEFAULT_INTEGER_COMPARISON_COST, DEFAULT_LARGE_TRANSACTION_GAS_LIMIT, + DEFAULT_LOAD_COST, DEFAULT_LOCAL_COST, DEFAULT_MAX_PAYMENT_MOTES, DEFAULT_MAX_STACK_HEIGHT, + DEFAULT_MIN_TRANSFER_MOTES, DEFAULT_MUL_COST, DEFAULT_NEW_DICTIONARY_COST, DEFAULT_NOP_COST, + DEFAULT_STORE_COST, DEFAULT_TRANSFER_COST, DEFAULT_UNREACHABLE_COST, DEFAULT_WASM_MAX_MEMORY, }; pub use contract_wasm::{ContractWasm, ContractWasmHash}; #[doc(inline)] @@ -187,15 +186,14 @@ pub use timestamp::{TimeDiff, Timestamp}; #[cfg(any(feature = "std", test))] pub use transaction::GasLimited; pub use transaction::{ - AddressableEntityIdentifier, Approval, ApprovalsHash, Deploy, DeployDecodeFromJsonError, - DeployError, DeployExcessiveSizeError, DeployHash, DeployHeader, DeployId, - ExecutableDeployItem, ExecutableDeployItemIdentifier, ExecutionInfo, InitiatorAddr, + arg_handling, AddressableEntityIdentifier, Approval, ApprovalsHash, Deploy, + DeployDecodeFromJsonError, DeployError, DeployExcessiveSizeError, DeployHash, DeployHeader, + DeployId, ExecutableDeployItem, ExecutableDeployItemIdentifier, ExecutionInfo, InitiatorAddr, InvalidDeploy, InvalidTransaction, InvalidTransactionV1, NamedArg, PackageIdentifier, - PricingMode, RuntimeArgs, Transaction, TransactionCategory, TransactionEntryPoint, - TransactionHash, TransactionHeader, TransactionId, TransactionInvocationTarget, - TransactionRuntime, TransactionScheduling, TransactionTarget, TransactionV1, TransactionV1Body, - TransactionV1DecodeFromJsonError, TransactionV1Error, TransactionV1ExcessiveSizeError, - TransactionV1Hash, TransactionV1Header, TransferTarget, + PricingMode, PricingModeError, RuntimeArgs, Transaction, TransactionEntryPoint, + TransactionHash, TransactionId, TransactionInvocationTarget, TransactionRuntime, + TransactionScheduling, TransactionTarget, TransactionV1, TransactionV1DecodeFromJsonError, + TransactionV1Error, TransactionV1ExcessiveSizeError, TransactionV1Hash, TransferTarget, }; #[cfg(any(feature = "std", test))] pub use transaction::{ @@ -214,8 +212,14 @@ pub use validator_change::ValidatorChange; pub const MINT_LANE_ID: u8 = 0; /// The lane identifier for the native auction interaction. pub const AUCTION_LANE_ID: u8 = 1; -/// The lane identifier for the special Wasm `install_upgrade` lane. +/// The lane identifier for the install/upgrade auction interaction. pub const INSTALL_UPGRADE_LANE_ID: u8 = 2; +/// The lane identifier for large wasms. +pub const LARGE_WASM_LANE_ID: u8 = 3; +/// The lane identifier for medium wasms. +pub const MEDIUM_WASM_LANE_ID: u8 = 4; +/// The lane identifier for small wasms. +pub const SMALL_WASM_LANE_ID: u8 = 5; /// OS page size. #[cfg(feature = "std")] diff --git a/types/src/transaction.rs b/types/src/transaction.rs index 7e11215eba..1f06f69ab1 100644 --- a/types/src/transaction.rs +++ b/types/src/transaction.rs @@ -5,16 +5,14 @@ mod deploy; mod error; mod execution_info; mod initiator_addr; -#[cfg(any(feature = "std", test))] +#[cfg(any(feature = "std", test, feature = "testing"))] mod initiator_addr_and_secret_key; mod package_identifier; mod pricing_mode; mod runtime_args; mod serialization; -mod transaction_category; mod transaction_entry_point; mod transaction_hash; -mod transaction_header; mod transaction_id; mod transaction_invocation_target; mod transaction_runtime; @@ -28,6 +26,8 @@ use core::fmt::{self, Debug, Display, Formatter}; #[cfg(any(feature = "std", test))] use std::hash::Hash; +#[cfg(feature = "json-schema")] +use crate::URef; #[cfg(feature = "datasize")] use datasize::DataSize; #[cfg(feature = "json-schema")] @@ -42,12 +42,10 @@ use tracing::error; #[cfg(any(all(feature = "std", feature = "testing"), test))] use crate::testing::TestRng; -#[cfg(feature = "json-schema")] -use crate::URef; use crate::{ account::AccountHash, bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, - Digest, Phase, SecretKey, TimeDiff, Timestamp, + Digest, SecretKey, TimeDiff, Timestamp, }; #[cfg(any(feature = "std", test))] use crate::{Chainspec, Gas, Motes}; @@ -63,23 +61,25 @@ pub use deploy::{DeployBuilder, DeployBuilderError}; pub use error::InvalidTransaction; pub use execution_info::ExecutionInfo; pub use initiator_addr::InitiatorAddr; -#[cfg(any(feature = "std", test))] -use initiator_addr_and_secret_key::InitiatorAddrAndSecretKey; +#[cfg(any(feature = "std", feature = "testing", test))] +pub(crate) use initiator_addr_and_secret_key::InitiatorAddrAndSecretKey; pub use package_identifier::PackageIdentifier; -pub use pricing_mode::PricingMode; +pub use pricing_mode::{PricingMode, PricingModeError}; pub use runtime_args::{NamedArg, RuntimeArgs}; pub use transaction_entry_point::TransactionEntryPoint; pub use transaction_hash::TransactionHash; -pub use transaction_header::TransactionHeader; pub use transaction_id::TransactionId; pub use transaction_invocation_target::TransactionInvocationTarget; pub use transaction_runtime::TransactionRuntime; pub use transaction_scheduling::TransactionScheduling; pub use transaction_target::TransactionTarget; +#[cfg(any(feature = "std", feature = "testing", feature = "gens", test))] +pub(crate) use transaction_v1::fields_container::FieldsContainer; +#[cfg(any(feature = "testing", feature = "gens", test))] +pub use transaction_v1::TransactionV1Payload; pub use transaction_v1::{ - InvalidTransactionV1, TransactionCategory, TransactionV1, TransactionV1Body, - TransactionV1DecodeFromJsonError, TransactionV1Error, TransactionV1ExcessiveSizeError, - TransactionV1Hash, TransactionV1Header, + arg_handling, InvalidTransactionV1, TransactionV1, TransactionV1DecodeFromJsonError, + TransactionV1Error, TransactionV1ExcessiveSizeError, TransactionV1Hash, }; #[cfg(any(feature = "std", test))] pub use transaction_v1::{TransactionV1Builder, TransactionV1BuilderError}; @@ -138,14 +138,6 @@ impl Transaction { } } - /// Body hash. - pub fn body_hash(&self) -> Digest { - match self { - Transaction::Deploy(deploy) => *deploy.header().body_hash(), - Transaction::V1(v1) => *v1.header().body_hash(), - } - } - /// Size estimate. pub fn size_estimate(&self) -> usize { match self { @@ -158,7 +150,7 @@ impl Transaction { pub fn timestamp(&self) -> Timestamp { match self { Transaction::Deploy(deploy) => deploy.header().timestamp(), - Transaction::V1(v1) => v1.header().timestamp(), + Transaction::V1(v1) => v1.payload().timestamp(), } } @@ -166,7 +158,7 @@ impl Transaction { pub fn ttl(&self) -> TimeDiff { match self { Transaction::Deploy(deploy) => deploy.header().ttl(), - Transaction::V1(v1) => v1.header().ttl(), + Transaction::V1(v1) => v1.payload().ttl(), } } @@ -194,15 +186,15 @@ impl Transaction { Transaction::V1(v1) => v1.approvals().clone(), } } - - /// Returns the header. - pub fn header(&self) -> TransactionHeader { - match self { - Transaction::Deploy(deploy) => TransactionHeader::Deploy(deploy.header().clone()), - Transaction::V1(transaction) => TransactionHeader::V1(transaction.header().clone()), + /* + /// Returns the header. + pub fn header(&self) -> TransactionHeader { + match self { + Transaction::Deploy(deploy) => TransactionHeader::Deploy(deploy.header().clone()), + Transaction::V1(transaction) => TransactionHeader::V1(transaction.header().clone()), + } } - } - + */ /// Returns the computed approvals hash identifying this transaction's approvals. pub fn compute_approvals_hash(&self) -> Result { let approvals_hash = match self { @@ -212,6 +204,7 @@ impl Transaction { Ok(approvals_hash) } + /* TODO remove /// Turns `self` into an invalid `Transaction` by clearing the `chain_name`, invalidating the /// transaction hash. #[cfg(any(all(feature = "std", feature = "testing"), test))] @@ -221,6 +214,7 @@ impl Transaction { Transaction::V1(v1) => v1.invalidate(), } } + */ /// Returns the computed `TransactionId` uniquely identifying this transaction and its /// approvals. @@ -265,7 +259,7 @@ impl Transaction { pub fn expires(&self) -> Timestamp { match self { Transaction::Deploy(deploy) => deploy.header().expires(), - Transaction::V1(txn) => txn.header().expires(), + Transaction::V1(txn) => txn.payload().expires(), } } @@ -299,98 +293,55 @@ impl Transaction { } } - /// Returns `true` if `self` represents a native transfer deploy or a native V1 transaction. - pub fn is_native(&self) -> bool { - match self { - Transaction::Deploy(deploy) => deploy.is_transfer(), - Transaction::V1(v1_txn) => *v1_txn.target() == TransactionTarget::Native, - } - } - - /// Is this a transaction that should be sent to the v1 execution engine? - pub fn is_v1_wasm(&self) -> bool { + /// Is the transaction the legacy deploy variant. + pub fn is_legacy_transaction(&self) -> bool { match self { - Transaction::Deploy(deploy) => !deploy.is_transfer(), - Transaction::V1(v1) => v1.is_v1_wasm(), + Transaction::Deploy(_) => true, + Transaction::V1(_) => false, } } - /// Should this transaction use standard payment processing? - pub fn is_standard_payment(&self) -> bool { + #[cfg(any(all(feature = "std", feature = "testing"), test))] + /// Calcualates the gas limit for the transaction. + pub fn gas_limit(&self, chainspec: &Chainspec, lane_id: u8) -> Result { match self { - Transaction::Deploy(deploy) => deploy.payment().is_standard_payment(Phase::Payment), + Transaction::Deploy(deploy) => deploy + .gas_limit(chainspec) + .map_err(InvalidTransaction::from), Transaction::V1(v1) => { - if let PricingMode::Classic { - standard_payment, .. - } = v1.pricing_mode() - { - *standard_payment - } else { - true - } + let pricing_mode = v1.pricing_mode(); + let entry_point = v1 + .get_transaction_entry_point() + .map_err(InvalidTransaction::from)?; + pricing_mode + .gas_limit(chainspec, &entry_point, lane_id) + .map_err(InvalidTransaction::from) } } } - /// Should this transaction start in the initiating accounts context? - pub fn is_account_session(&self) -> bool { - match self { - Transaction::Deploy(deploy) => deploy.is_account_session(), - Transaction::V1(v1) => v1.is_account_session(), - } - } - - /// Authorization keys. - pub fn authorization_keys(&self) -> BTreeSet { + #[cfg(any(all(feature = "std", feature = "testing"), test))] + /// Returns a gas cost based upon the gas_limit, the gas price, + /// and the chainspec settings. + pub fn gas_cost( + &self, + chainspec: &Chainspec, + lane_id: u8, + gas_price: u8, + ) -> Result { match self { Transaction::Deploy(deploy) => deploy - .approvals() - .iter() - .map(|approval| approval.signer().to_account_hash()) - .collect(), - Transaction::V1(transaction_v1) => transaction_v1 - .approvals() - .iter() - .map(|approval| approval.signer().to_account_hash()) - .collect(), - } - } - - /// The session args. - pub fn session_args(&self) -> &RuntimeArgs { - match self { - Transaction::Deploy(deploy) => deploy.session().args(), - Transaction::V1(transaction_v1) => transaction_v1.body().args(), - } - } - - /// The entry point. - pub fn entry_point(&self) -> TransactionEntryPoint { - match self { - Transaction::Deploy(deploy) => deploy.session().entry_point_name().into(), - Transaction::V1(transaction_v1) => transaction_v1.entry_point().clone(), - } - } - - /// The transaction category. - pub fn transaction_category(&self) -> u8 { - match self { - Transaction::Deploy(deploy) => { - if deploy.is_transfer() { - TransactionCategory::Mint as u8 - } else { - TransactionCategory::Large as u8 - } + .gas_cost(chainspec, gas_price) + .map_err(InvalidTransaction::from), + Transaction::V1(v1) => { + let pricing_mode = v1.pricing_mode(); + let entry_point = v1 + .get_transaction_entry_point() + .map_err(InvalidTransaction::from)?; + pricing_mode + .gas_cost(chainspec, &entry_point, lane_id, gas_price) + .map_err(InvalidTransaction::from) } - Transaction::V1(v1) => v1.transaction_category(), - } - } - - /// Is the transaction the legacy deploy variant. - pub fn is_legacy_transaction(&self) -> bool { - match self { - Transaction::Deploy(_) => true, - Transaction::V1(_) => false, } } @@ -432,40 +383,6 @@ pub trait GasLimited { fn gas_price_tolerance(&self) -> Result; } -#[cfg(any(feature = "std", test))] -impl GasLimited for Transaction { - type Error = InvalidTransaction; - - fn gas_cost(&self, chainspec: &Chainspec, gas_price: u8) -> Result { - match self { - Transaction::Deploy(deploy) => deploy - .gas_cost(chainspec, gas_price) - .map_err(InvalidTransaction::from), - Transaction::V1(v1) => v1 - .gas_cost(chainspec, gas_price) - .map_err(InvalidTransaction::from), - } - } - - fn gas_limit(&self, chainspec: &Chainspec) -> Result { - match self { - Transaction::Deploy(deploy) => deploy - .gas_limit(chainspec) - .map_err(InvalidTransaction::from), - Transaction::V1(v1) => v1.gas_limit(chainspec).map_err(InvalidTransaction::from), - } - } - - fn gas_price_tolerance(&self) -> Result { - match self { - Transaction::Deploy(deploy) => deploy - .gas_price_tolerance() - .map_err(InvalidTransaction::from), - Transaction::V1(v1) => v1.gas_price_tolerance().map_err(InvalidTransaction::from), - } - } -} - impl From for Transaction { fn from(deploy: Deploy) -> Self { Self::Deploy(deploy) @@ -536,13 +453,12 @@ impl Display for Transaction { /// Proptest generators for [`Transaction`]. #[cfg(any(feature = "testing", feature = "gens", test))] pub mod gens { + use super::*; use proptest::{ array, prelude::{Arbitrary, Strategy}, }; - use super::*; - pub fn deploy_hash_arb() -> impl Strategy { array::uniform32(::arbitrary()).prop_map(DeployHash::from_raw) } @@ -594,3 +510,24 @@ mod tests { bytesrepr::test_serialization_roundtrip(&transaction); } } + +#[cfg(test)] +mod proptests { + use super::*; + use crate::{bytesrepr, gens::transaction_arb}; + use proptest::prelude::*; + + proptest! { + #[test] + fn bytesrepr_roundtrip(transaction in transaction_arb()) { + bytesrepr::test_serialization_roundtrip(&transaction); + } + + #[test] + fn json_roundtrip(transaction in transaction_arb()) { + let json_string = serde_json::to_string_pretty(&transaction).unwrap(); + let decoded = serde_json::from_str::(&json_string).unwrap(); + assert_eq!(transaction, decoded); + } + } +} diff --git a/types/src/transaction/deploy.rs b/types/src/transaction/deploy.rs index 74a99c821f..3177dfa052 100644 --- a/types/src/transaction/deploy.rs +++ b/types/src/transaction/deploy.rs @@ -62,12 +62,11 @@ use crate::{ }; #[cfg(any(feature = "std", test))] -use crate::{chainspec::PricingHandling, transaction::TransactionCategory, Chainspec}; +use crate::{chainspec::PricingHandling, Chainspec, LARGE_WASM_LANE_ID}; #[cfg(any(feature = "std", test))] use crate::{system::auction::ARG_AMOUNT, transaction::GasLimited, Gas, Motes, U512}; #[cfg(any(feature = "std", test))] pub use deploy_builder::{DeployBuilder, DeployBuilderError}; -pub use deploy_category::DeployCategory; pub use deploy_hash::DeployHash; pub use deploy_header::DeployHeader; pub use deploy_id::DeployId; @@ -404,9 +403,12 @@ impl Deploy { at: Timestamp, ) -> Result<(), InvalidDeploy> { let config = &chainspec.transaction_config; + // We're assuming that Deploy can have a maximum size of an InstallUpgrade transaction. + // We're passing 0 as transaction size since determining max transaction size for + // InstallUpgrade doesn't rely on the size of transaction let max_transaction_size = config .transaction_v1_config - .get_max_serialized_length(TransactionCategory::Large as u8); + .get_max_serialized_length(LARGE_WASM_LANE_ID); self.is_valid_size(max_transaction_size as u32)?; let header = self.header(); @@ -1319,7 +1321,7 @@ impl GasLimited for Deploy { let computation_limit = if self.is_transfer() { costs.mint_costs().transfer as u64 } else { - chainspec.get_max_gas_limit_by_category(TransactionCategory::Large as u8) + chainspec.get_max_gas_limit_by_category(LARGE_WASM_LANE_ID) }; Gas::new(computation_limit) } // legacy deploys do not support reservations diff --git a/types/src/transaction/initiator_addr_and_secret_key.rs b/types/src/transaction/initiator_addr_and_secret_key.rs index 20b565b401..d174a81d87 100644 --- a/types/src/transaction/initiator_addr_and_secret_key.rs +++ b/types/src/transaction/initiator_addr_and_secret_key.rs @@ -2,7 +2,7 @@ use crate::{InitiatorAddr, PublicKey, SecretKey}; /// Used when constructing a deploy or transaction. #[derive(Debug)] -pub(super) enum InitiatorAddrAndSecretKey<'a> { +pub(crate) enum InitiatorAddrAndSecretKey<'a> { /// Provides both the initiator address and the secret key (not necessarily for the same /// initiator address) used to sign the deploy or transaction. Both { diff --git a/types/src/transaction/pricing_mode.rs b/types/src/transaction/pricing_mode.rs index 159f523eac..5036d9a88e 100644 --- a/types/src/transaction/pricing_mode.rs +++ b/types/src/transaction/pricing_mode.rs @@ -9,9 +9,12 @@ use rand::Rng; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use super::serialization::CalltableSerializationEnvelope; #[cfg(doc)] use super::Transaction; +use super::{ + serialization::CalltableSerializationEnvelope, InvalidTransaction, InvalidTransactionV1, + TransactionEntryPoint, +}; #[cfg(any(feature = "testing", test))] use crate::testing::TestRng; use crate::{ @@ -22,6 +25,8 @@ use crate::{ transaction::serialization::CalltableSerializationEnvelopeBuilder, Digest, }; +#[cfg(any(feature = "std", test))] +use crate::{Chainspec, Gas, Motes, AUCTION_LANE_ID, MINT_LANE_ID, U512}; /// The pricing mode of a [`Transaction`]. #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize, Debug)] @@ -48,6 +53,14 @@ pub enum PricingMode { /// The cost of the transaction is determined by the cost table, per the /// transaction category. Fixed { + /// User-specified additional computation factor (minimum 0). If "0" is provided, + /// no additional logic is applied to the computation limit. Each value above "0" + /// tells the node that it needs to treat the transaction as if it uses more gas + /// than it's serialized size indicates. Each "1" will increase the "wasm lane" + /// size bucket for this transaction by 1. So if the size of the transaction + /// indicates bucket "0" and "additional_computation_factor = 2", the transaction + /// will be treated as a "2". + additional_computation_factor: u8, /// User-specified gas_price tolerance (minimum 1). /// This is interpreted to mean "do not include this transaction in a block /// if the current gas price is greater than this number" @@ -73,6 +86,7 @@ impl PricingMode { }, 1 => PricingMode::Fixed { gas_price_tolerance: rng.gen(), + additional_computation_factor: 1, }, 2 => PricingMode::Reserved { receipt: rng.gen() }, _ => unreachable!(), @@ -95,10 +109,12 @@ impl PricingMode { } PricingMode::Fixed { gas_price_tolerance, + additional_computation_factor, } => { vec![ crate::bytesrepr::U8_SERIALIZED_LENGTH, gas_price_tolerance.serialized_length(), + additional_computation_factor.serialized_length(), ] } PricingMode::Reserved { receipt } => { @@ -109,6 +125,155 @@ impl PricingMode { } } } + + #[cfg(any(feature = "std", test))] + /// Returns the gas limit. + pub fn gas_limit( + &self, + chainspec: &Chainspec, + entry_point: &TransactionEntryPoint, + lane_id: u8, + ) -> Result { + let costs = chainspec.system_costs_config; + let gas = match self { + PricingMode::Classic { payment_amount, .. } => Gas::new(*payment_amount), + PricingMode::Fixed { .. } => { + let computation_limit = { + if lane_id == MINT_LANE_ID { + // Because we currently only support one native mint interaction, + // native transfer, we can short circuit to return that value. + // However if other direct mint interactions are supported + // in the future (such as the upcoming burn feature), + // this logic will need to be expanded to self.mint_costs().field? + // for the value for each verb...see how auction is set up below. + costs.mint_costs().transfer as u64 + } else if lane_id == AUCTION_LANE_ID { + let amount = match entry_point { + TransactionEntryPoint::Call => { + return Err(PricingModeError::EntryPointCannotBeCall) + } + TransactionEntryPoint::Custom(_) | TransactionEntryPoint::Transfer => { + return Err(PricingModeError::EntryPointCannotBeCustom { + entry_point: entry_point.clone(), + }); + } + TransactionEntryPoint::AddBid | TransactionEntryPoint::ActivateBid => { + costs.auction_costs().add_bid.into() + } + TransactionEntryPoint::WithdrawBid => { + costs.auction_costs().withdraw_bid.into() + } + TransactionEntryPoint::Delegate => { + costs.auction_costs().delegate.into() + } + TransactionEntryPoint::Undelegate => { + costs.auction_costs().undelegate.into() + } + TransactionEntryPoint::Redelegate => { + costs.auction_costs().redelegate.into() + } + TransactionEntryPoint::ChangeBidPublicKey => { + costs.auction_costs().change_bid_public_key + } + TransactionEntryPoint::AddReservations => { + costs.auction_costs().add_reservations.into() + } + TransactionEntryPoint::CancelReservations => { + costs.auction_costs().cancel_reservations.into() + } + }; + amount + } else { + chainspec.get_max_gas_limit_by_category(lane_id) + } + }; + Gas::new(U512::from(computation_limit)) + } + PricingMode::Reserved { receipt } => { + return Err(PricingModeError::InvalidPricingMode { + price_mode: PricingMode::Reserved { receipt: *receipt }, + }); + } + }; + Ok(gas) + } + + #[cfg(any(feature = "std", test))] + /// Returns gas cost. + pub fn gas_cost( + &self, + chainspec: &Chainspec, + entry_point: &TransactionEntryPoint, + lane_id: u8, + gas_price: u8, + ) -> Result { + let gas_limit = self.gas_limit(chainspec, entry_point, lane_id)?; + let motes = match self { + PricingMode::Classic { .. } | PricingMode::Fixed { .. } => { + Motes::from_gas(gas_limit, gas_price) + .ok_or(PricingModeError::UnableToCalculateGasCost)? + } + PricingMode::Reserved { .. } => { + Motes::zero() // prepaid + } + }; + Ok(motes) + } + + /// Returns gas cost. + pub fn additional_computation_factor(&self) -> u8 { + match self { + PricingMode::Classic { .. } => 0, + PricingMode::Fixed { + additional_computation_factor, + .. + } => *additional_computation_factor, + PricingMode::Reserved { .. } => 0, + } + } +} + +///Errors that can occur when calling PricingMode functions +pub enum PricingModeError { + /// The entry point for this transaction target cannot be `call`. + EntryPointCannotBeCall, + /// The entry point for this transaction target cannot be `TransactionEntryPoint::Custom`. + EntryPointCannotBeCustom { + /// The invalid entry point. + entry_point: TransactionEntryPoint, + }, + /// Invalid combination of pricing handling and pricing mode. + InvalidPricingMode { + /// The pricing mode as specified by the transaction. + price_mode: PricingMode, + }, + /// Unable to calculate gas cost. + UnableToCalculateGasCost, +} + +impl From for InvalidTransaction { + fn from(err: PricingModeError) -> Self { + InvalidTransaction::V1(err.into()) + } +} + +impl From for InvalidTransactionV1 { + fn from(err: PricingModeError) -> Self { + match err { + PricingModeError::EntryPointCannotBeCall => { + InvalidTransactionV1::EntryPointCannotBeCall + } + PricingModeError::EntryPointCannotBeCustom { entry_point } => { + InvalidTransactionV1::EntryPointCannotBeCustom { entry_point } + } + PricingModeError::InvalidPricingMode { price_mode } => { + InvalidTransactionV1::InvalidPricingMode { price_mode } + } + PricingModeError::UnableToCalculateGasCost => { + InvalidTransactionV1::UnableToCalculateGasCost + } + } + } } const TAG_FIELD_INDEX: u16 = 0; @@ -119,6 +284,7 @@ const CLASSIC_STANDARD_PAYMENT_INDEX: u16 = 3; const FIXED_VARIANT_TAG: u8 = 1; const FIXED_GAS_PRICE_TOLERANCE_INDEX: u16 = 1; +const FIXED_ADDITIONAL_COMPUTATION_FACTOR_INDEX: u16 = 2; const RESERVED_VARIANT_TAG: u8 = 2; const RESERVED_RECEIPT_INDEX: u16 = 1; @@ -138,9 +304,14 @@ impl ToBytes for PricingMode { .binary_payload_bytes(), PricingMode::Fixed { gas_price_tolerance, + additional_computation_factor, } => CalltableSerializationEnvelopeBuilder::new(self.serialized_field_lengths())? .add_field(TAG_FIELD_INDEX, &FIXED_VARIANT_TAG)? .add_field(FIXED_GAS_PRICE_TOLERANCE_INDEX, &gas_price_tolerance)? + .add_field( + FIXED_ADDITIONAL_COMPUTATION_FACTOR_INDEX, + &additional_computation_factor, + )? .binary_payload_bytes(), PricingMode::Reserved { receipt } => { CalltableSerializationEnvelopeBuilder::new(self.serialized_field_lengths())? @@ -185,11 +356,16 @@ impl FromBytes for PricingMode { let window = window.ok_or(Formatting)?; window.verify_index(FIXED_GAS_PRICE_TOLERANCE_INDEX)?; let (gas_price_tolerance, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(Formatting)?; + window.verify_index(FIXED_ADDITIONAL_COMPUTATION_FACTOR_INDEX)?; + let (additional_computation_factor, window) = + window.deserialize_and_maybe_next::()?; if window.is_some() { return Err(Formatting); } Ok(PricingMode::Fixed { gas_price_tolerance, + additional_computation_factor, }) } RESERVED_VARIANT_TAG => { @@ -224,7 +400,12 @@ impl Display for PricingMode { PricingMode::Reserved { receipt } => write!(formatter, "reserved: {}", receipt), PricingMode::Fixed { gas_price_tolerance, - } => write!(formatter, "fixed pricing {}", gas_price_tolerance), + additional_computation_factor, + } => write!( + formatter, + "fixed pricing {} {}", + gas_price_tolerance, additional_computation_factor + ), } } } diff --git a/types/src/transaction/transaction_header.rs b/types/src/transaction/transaction_header.rs deleted file mode 100644 index 68b5d7f3bd..0000000000 --- a/types/src/transaction/transaction_header.rs +++ /dev/null @@ -1,129 +0,0 @@ -use alloc::vec::Vec; -use core::fmt::{self, Display, Formatter}; - -#[cfg(feature = "datasize")] -use datasize::DataSize; -#[cfg(feature = "json-schema")] -use schemars::JsonSchema; -#[cfg(any(feature = "std", test))] -use serde::{Deserialize, Serialize}; - -use super::{DeployHeader, TransactionV1Header}; -use crate::{ - bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, - InitiatorAddr, -}; - -const DEPLOY_TAG: u8 = 0; -const V1_TAG: u8 = 1; - -/// A versioned wrapper for a transaction header or deploy header. -#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)] -#[cfg_attr( - any(feature = "std", test), - derive(Serialize, Deserialize), - serde(deny_unknown_fields) -)] -#[cfg_attr(feature = "datasize", derive(DataSize))] -#[cfg_attr(feature = "json-schema", derive(JsonSchema))] -pub enum TransactionHeader { - /// A deploy header. - Deploy(DeployHeader), - /// A version 1 transaction header. - #[cfg_attr(any(feature = "std", test), serde(rename = "Version1"))] - V1(TransactionV1Header), -} - -impl TransactionHeader { - /// Return the initiator addr of this transaction. - pub fn initiator_addr(&self) -> InitiatorAddr { - match self { - TransactionHeader::Deploy(header) => header.account().clone().into(), - TransactionHeader::V1(header) => header.initiator_addr().clone(), - } - } -} - -impl From for TransactionHeader { - fn from(header: DeployHeader) -> Self { - Self::Deploy(header) - } -} - -impl From for TransactionHeader { - fn from(header: TransactionV1Header) -> Self { - Self::V1(header) - } -} - -impl Display for TransactionHeader { - fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { - match self { - TransactionHeader::Deploy(hash) => Display::fmt(hash, formatter), - TransactionHeader::V1(hash) => Display::fmt(hash, formatter), - } - } -} - -impl ToBytes for TransactionHeader { - fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { - match self { - TransactionHeader::Deploy(header) => { - DEPLOY_TAG.write_bytes(writer)?; - header.write_bytes(writer) - } - TransactionHeader::V1(header) => { - V1_TAG.write_bytes(writer)?; - header.write_bytes(writer) - } - } - } - - fn to_bytes(&self) -> Result, bytesrepr::Error> { - let mut buffer = bytesrepr::allocate_buffer(self)?; - self.write_bytes(&mut buffer)?; - Ok(buffer) - } - - fn serialized_length(&self) -> usize { - U8_SERIALIZED_LENGTH - + match self { - TransactionHeader::Deploy(header) => header.serialized_length(), - TransactionHeader::V1(header) => header.serialized_length(), - } - } -} - -impl FromBytes for TransactionHeader { - fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { - let (tag, remainder) = u8::from_bytes(bytes)?; - match tag { - DEPLOY_TAG => { - let (header, remainder) = DeployHeader::from_bytes(remainder)?; - Ok((TransactionHeader::Deploy(header), remainder)) - } - V1_TAG => { - let (header, remainder) = TransactionV1Header::from_bytes(remainder)?; - Ok((TransactionHeader::V1(header), remainder)) - } - _ => Err(bytesrepr::Error::Formatting), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{testing::TestRng, Deploy, TransactionV1}; - - #[test] - fn bytesrepr_roundtrip() { - let rng = &mut TestRng::new(); - - let header = TransactionHeader::from(Deploy::random(rng).take_header()); - bytesrepr::test_serialization_roundtrip(&header); - - let header = TransactionHeader::from(TransactionV1::random(rng).take_header()); - bytesrepr::test_serialization_roundtrip(&header); - } -} diff --git a/types/src/transaction/transaction_target.rs b/types/src/transaction/transaction_target.rs index e1e8d4f284..f8d83a4c73 100644 --- a/types/src/transaction/transaction_target.rs +++ b/types/src/transaction/transaction_target.rs @@ -43,10 +43,12 @@ pub enum TransactionTarget { }, /// The execution target is the included module bytes, i.e. compiled Wasm. Session { - /// The compiled Wasm. - module_bytes: Bytes, + /// Flag determining if the Wasm is an install/upgrade. + is_install_upgrade: bool, /// The execution runtime to use. runtime: TransactionRuntime, + /// The compiled Wasm. + module_bytes: Bytes, }, } @@ -62,8 +64,13 @@ impl TransactionTarget { } /// Returns a new `TransactionTarget::Session`. - pub fn new_session(module_bytes: Bytes, runtime: TransactionRuntime) -> Self { + pub fn new_session( + is_install_upgrade: bool, + module_bytes: Bytes, + runtime: TransactionRuntime, + ) -> Self { TransactionTarget::Session { + is_install_upgrade, module_bytes, runtime, } @@ -82,13 +89,15 @@ impl TransactionTarget { ] } TransactionTarget::Session { - module_bytes, + is_install_upgrade, runtime, + module_bytes, } => { vec![ crate::bytesrepr::U8_SERIALIZED_LENGTH, - module_bytes.serialized_length(), + is_install_upgrade.serialized_length(), runtime.serialized_length(), + module_bytes.serialized_length(), ] } } @@ -106,7 +115,12 @@ impl TransactionTarget { 2 => { let mut buffer = vec![0u8; rng.gen_range(0..100)]; rng.fill_bytes(buffer.as_mut()); - TransactionTarget::new_session(Bytes::from(buffer), TransactionRuntime::VmCasperV1) + let is_install_upgrade = rng.gen(); + TransactionTarget::new_session( + is_install_upgrade, + Bytes::from(buffer), + TransactionRuntime::VmCasperV1, + ) } _ => unreachable!(), } @@ -122,8 +136,9 @@ const STORED_ID_INDEX: u16 = 1; const STORED_RUNTIME_INDEX: u16 = 2; const SESSION_VARIANT: u8 = 2; -const SESSION_MODULE_BYTES_INDEX: u16 = 1; +const SESSION_IS_INSTALL_INDEX: u16 = 1; const SESSION_RUNTIME_INDEX: u16 = 2; +const SESSION_MODULE_BYTES_INDEX: u16 = 3; impl ToBytes for TransactionTarget { fn to_bytes(&self) -> Result, Error> { @@ -141,12 +156,14 @@ impl ToBytes for TransactionTarget { .binary_payload_bytes() } TransactionTarget::Session { + is_install_upgrade, module_bytes, runtime, } => CalltableSerializationEnvelopeBuilder::new(self.serialized_field_lengths())? .add_field(TAG_FIELD_INDEX, &SESSION_VARIANT)? - .add_field(SESSION_MODULE_BYTES_INDEX, &module_bytes)? + .add_field(SESSION_IS_INSTALL_INDEX, &is_install_upgrade)? .add_field(SESSION_RUNTIME_INDEX, &runtime)? + .add_field(SESSION_MODULE_BYTES_INDEX, &module_bytes)? .binary_payload_bytes(), } } @@ -158,7 +175,7 @@ impl ToBytes for TransactionTarget { impl FromBytes for TransactionTarget { fn from_bytes(bytes: &[u8]) -> Result<(TransactionTarget, &[u8]), Error> { - let (binary_payload, remainder) = CalltableSerializationEnvelope::from_bytes(3, bytes)?; + let (binary_payload, remainder) = CalltableSerializationEnvelope::from_bytes(4, bytes)?; let window = binary_payload.start_consuming()?.ok_or(Formatting)?; window.verify_index(TAG_FIELD_INDEX)?; let (tag, window) = window.deserialize_and_maybe_next::()?; @@ -185,16 +202,20 @@ impl FromBytes for TransactionTarget { } SESSION_VARIANT => { let window = window.ok_or(Formatting)?; - window.verify_index(SESSION_MODULE_BYTES_INDEX)?; - let (module_bytes, window) = window.deserialize_and_maybe_next::()?; + window.verify_index(SESSION_IS_INSTALL_INDEX)?; + let (is_install_upgrade, window) = window.deserialize_and_maybe_next::()?; let window = window.ok_or(Formatting)?; window.verify_index(SESSION_RUNTIME_INDEX)?; let (runtime, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(Formatting)?; + window.verify_index(SESSION_MODULE_BYTES_INDEX)?; + let (module_bytes, window) = window.deserialize_and_maybe_next::()?; if window.is_some() { return Err(Formatting); } Ok(TransactionTarget::Session { + is_install_upgrade, module_bytes, runtime, }) @@ -213,13 +234,15 @@ impl Display for TransactionTarget { write!(formatter, "stored({}, {})", id, runtime) } TransactionTarget::Session { + is_install_upgrade, module_bytes, runtime, } => write!( formatter, - "session({} module bytes, {})", + "session({} module bytes, runtime: {}, is_install_upgrade: {})", module_bytes.len(), - runtime + runtime, + is_install_upgrade, ), } } @@ -235,6 +258,7 @@ impl Debug for TransactionTarget { .field("runtime", runtime) .finish(), TransactionTarget::Session { + is_install_upgrade, module_bytes, runtime, } => { @@ -249,6 +273,7 @@ impl Debug for TransactionTarget { .debug_struct("Session") .field("module_bytes", &BytesLen(module_bytes.len())) .field("runtime", runtime) + .field("is_install_upgrade", is_install_upgrade) .finish() } } diff --git a/types/src/transaction/transaction_v1.rs b/types/src/transaction/transaction_v1.rs index ddbf53c2ee..b2fd249055 100644 --- a/types/src/transaction/transaction_v1.rs +++ b/types/src/transaction/transaction_v1.rs @@ -1,20 +1,32 @@ +pub mod arg_handling; mod errors_v1; -mod transaction_v1_body; +pub mod fields_container; #[cfg(any(feature = "std", test))] mod transaction_v1_builder; -mod transaction_v1_category; mod transaction_v1_hash; -mod transaction_v1_header; +pub mod transaction_v1_payload; -#[cfg(any(all(feature = "std", feature = "testing"), test))] -use alloc::string::ToString; -use alloc::{collections::BTreeSet, vec::Vec}; -use core::{ - cmp, - fmt::{self, Debug, Display, Formatter}, - hash, +use crate::{ + bytesrepr::{self, Error, FromBytes, ToBytes}, + crypto, }; +#[cfg(any(feature = "testing", test))] +use crate::{ + testing::TestRng, AUCTION_LANE_ID, INSTALL_UPGRADE_LANE_ID, LARGE_WASM_LANE_ID, MINT_LANE_ID, +}; +#[cfg(any(feature = "std", test, feature = "testing"))] +use alloc::collections::BTreeMap; +use alloc::{collections::BTreeSet, vec::Vec}; +use errors_v1::FieldDeserializationError; +#[cfg(any(feature = "testing", test))] +use fields_container::{ENTRY_POINT_MAP_KEY, TARGET_MAP_KEY}; +use tracing::debug; +pub use transaction_v1_payload::TransactionV1Payload; +#[cfg(any(feature = "std", feature = "testing", test))] +use super::InitiatorAddrAndSecretKey; +#[cfg(any(all(feature = "std", feature = "testing"), test))] +use super::{TransactionEntryPoint, TransactionTarget}; #[cfg(feature = "datasize")] use datasize::DataSize; #[cfg(any(feature = "once_cell", test))] @@ -23,58 +35,36 @@ use once_cell::sync::OnceCell; use schemars::JsonSchema; #[cfg(any(feature = "std", test))] use serde::{Deserialize, Serialize}; -use tracing::debug; use super::{ serialization::{CalltableSerializationEnvelope, CalltableSerializationEnvelopeBuilder}, - Approval, ApprovalsHash, InitiatorAddr, PricingMode, TransactionEntryPoint, - TransactionScheduling, TransactionTarget, -}; -#[cfg(any(feature = "std", test))] -use super::{GasLimited, InitiatorAddrAndSecretKey}; -#[cfg(any(feature = "std", test))] -use crate::chainspec::Chainspec; -#[cfg(any(all(feature = "std", feature = "testing"), test))] -use crate::chainspec::PricingHandling; -use crate::{ - bytesrepr::{ - Error::{self, Formatting}, - FromBytes, ToBytes, - }, - crypto, Digest, DisplayIter, RuntimeArgs, SecretKey, TimeDiff, Timestamp, TransactionRuntime, + Approval, ApprovalsHash, InitiatorAddr, PricingMode, }; +#[cfg(any(feature = "std", feature = "testing", test))] +use crate::bytesrepr::Bytes; +use crate::{Digest, DisplayIter, SecretKey, TimeDiff, Timestamp}; -#[cfg(any(all(feature = "std", feature = "testing"), test))] -use crate::TransactionConfig; -#[cfg(any(feature = "std", test))] -use crate::{Gas, Motes, U512}; pub use errors_v1::{ DecodeFromJsonErrorV1 as TransactionV1DecodeFromJsonError, ErrorV1 as TransactionV1Error, ExcessiveSizeErrorV1 as TransactionV1ExcessiveSizeError, InvalidTransaction as InvalidTransactionV1, }; -pub use transaction_v1_body::TransactionV1Body; #[cfg(any(feature = "std", test))] pub use transaction_v1_builder::{TransactionV1Builder, TransactionV1BuilderError}; -pub use transaction_v1_category::TransactionCategory; pub use transaction_v1_hash::TransactionV1Hash; -pub use transaction_v1_header::TransactionV1Header; - -#[cfg(any(all(feature = "std", feature = "testing"), test))] -use crate::testing::TestRng; -const TRANSACTION_V1_SERIALIZATION_VERSION: u8 = 1; +use core::{ + cmp, + fmt::{self, Debug, Display, Formatter}, + hash, +}; -const SERIALIZATION_VERSION_INDEX: u16 = 0; -const HASH_FIELD_META_INDEX: u16 = 1; -const HEADER_FIELD_META_INDEX: u16 = 2; -const BODY_FIELD_META_INDEX: u16 = 3; -const APPROVALS_FIELD_META_INDEX: u16 = 4; +const HASH_FIELD_INDEX: u16 = 0; +const PAYLOAD_FIELD_INDEX: u16 = 1; +const APPROVALS_FIELD_INDEX: u16 = 2; /// A unit of work sent by a client to the network, which when executed can cause global state to /// be altered. -/// -/// To construct a new `TransactionV1`, use a [`TransactionV1Builder`]. #[derive(Clone, Eq, Debug)] #[cfg_attr( any(feature = "std", test), @@ -91,10 +81,8 @@ const APPROVALS_FIELD_META_INDEX: u16 = 4; ) )] pub struct TransactionV1 { - serialization_version: u8, hash: TransactionV1Hash, - header: TransactionV1Header, - body: TransactionV1Body, + payload: TransactionV1Payload, approvals: BTreeSet, #[cfg_attr(any(all(feature = "std", feature = "once_cell"), test), serde(skip))] #[cfg_attr( @@ -106,50 +94,32 @@ pub struct TransactionV1 { } impl TransactionV1 { - fn serialized_field_lengths(&self) -> Vec { - let serialization_version_len = TRANSACTION_V1_SERIALIZATION_VERSION.serialized_length(); - let approvals_len = self.approvals.serialized_length(); - let hash_len = self.hash.serialized_length(); - let header_len = self.header.serialized_length(); - let body_len = self.body.serialized_length(); - vec![ - serialization_version_len, - hash_len, - header_len, - body_len, - approvals_len, - ] - } - /// Called by the `TransactionV1Builder` to construct a new `TransactionV1`. - #[cfg(any(feature = "std", test))] - pub(super) fn build( + #[cfg(any(feature = "std", test, feature = "testing"))] + pub(crate) fn build( chain_name: String, timestamp: Timestamp, ttl: TimeDiff, - body: TransactionV1Body, pricing_mode: PricingMode, + fields: BTreeMap, initiator_addr_and_secret_key: InitiatorAddrAndSecretKey, ) -> TransactionV1 { let initiator_addr = initiator_addr_and_secret_key.initiator_addr(); - let body_hash = Digest::hash( - body.to_bytes() - .unwrap_or_else(|error| panic!("should serialize body: {}", error)), - ); - let header = TransactionV1Header::new( + let transaction_v1_payload = TransactionV1Payload::new( chain_name, timestamp, ttl, - body_hash, pricing_mode, initiator_addr, + fields, + ); + let hash = Digest::hash( + transaction_v1_payload + .to_bytes() + .unwrap_or_else(|error| panic!("should serialize body: {}", error)), ); - - let hash = header.compute_hash(); let mut transaction = TransactionV1 { - serialization_version: TRANSACTION_V1_SERIALIZATION_VERSION, - hash, - header, - body, + hash: hash.into(), + payload: transaction_v1_payload, approvals: BTreeSet::new(), #[cfg(any(feature = "once_cell", test))] is_verified: OnceCell::new(), @@ -161,341 +131,76 @@ impl TransactionV1 { transaction } - /// Returns the hash identifying this transaction. + /// Adds a signature of this transaction's hash to its approvals. + pub fn sign(&mut self, secret_key: &SecretKey) { + let approval = Approval::create(&self.hash.into(), secret_key); + self.approvals.insert(approval); + } + + /// Returns the `ApprovalsHash` of this transaction's approvals. pub fn hash(&self) -> &TransactionV1Hash { &self.hash } + /// Returns the internal payload of this transaction. + pub fn payload(&self) -> &TransactionV1Payload { + &self.payload + } + + /// Returns transactions approvals. + pub fn approvals(&self) -> &BTreeSet { + &self.approvals + } + + /// Returns the address of the initiator of the transaction. + pub fn initiator_addr(&self) -> &InitiatorAddr { + self.payload.initiator_addr() + } + /// Returns the name of the chain the transaction should be executed on. pub fn chain_name(&self) -> &str { - self.header.chain_name() + self.payload.chain_name() } /// Returns the creation timestamp of the transaction. pub fn timestamp(&self) -> Timestamp { - self.header.timestamp() + self.payload.timestamp() } /// Returns the duration after the creation timestamp for which the transaction will stay valid. /// /// After this duration has ended, the transaction will be considered expired. pub fn ttl(&self) -> TimeDiff { - self.header.ttl() + self.payload.ttl() } /// Returns `true` if the transaction has expired. pub fn expired(&self, current_instant: Timestamp) -> bool { - self.header.expired(current_instant) + self.payload.expired(current_instant) } /// Returns the pricing mode for the transaction. pub fn pricing_mode(&self) -> &PricingMode { - self.header.pricing_mode() - } - - /// Returns the address of the initiator of the transaction. - pub fn initiator_addr(&self) -> &InitiatorAddr { - self.header.initiator_addr() - } - - /// Returns a reference to the header of this transaction. - pub fn header(&self) -> &TransactionV1Header { - &self.header - } - - /// Consumes `self`, returning the header of this transaction. - pub fn take_header(self) -> TransactionV1Header { - self.header - } - - /// Returns the runtime args of the transaction. - pub fn args(&self) -> &RuntimeArgs { - self.body.args() - } - - /// Consumes `self`, returning the runtime args of the transaction. - pub fn take_args(self) -> RuntimeArgs { - self.body.take_args() - } - - /// Returns the target of the transaction. - pub fn target(&self) -> &TransactionTarget { - self.body.target() - } - - /// Returns the entry point of the transaction. - pub fn entry_point(&self) -> &TransactionEntryPoint { - self.body.entry_point() - } - - /// Returns the scheduling kind of the transaction. - pub fn scheduling(&self) -> &TransactionScheduling { - self.body.scheduling() - } - - /// Returns the body of this transaction. - pub fn body(&self) -> &TransactionV1Body { - &self.body - } - - /// Returns true if this transaction is a native mint interaction. - pub fn is_native_mint(&self) -> bool { - self.body().is_native_mint() - } - - /// Returns true if this transaction is a native auction interaction. - pub fn is_native_auction(&self) -> bool { - self.body().is_native_auction() - } - - /// Returns true if this transaction is a smart contract installer or upgrader. - pub fn is_install_or_upgrade(&self) -> bool { - self.body().is_install_or_upgrade() - } - - /// Returns the transaction category. - pub fn transaction_category(&self) -> u8 { - self.body.transaction_category() - } - - /// Does this transaction have wasm targeting the v1 vm. - pub fn is_v1_wasm(&self) -> bool { - match self.target() { - TransactionTarget::Native => false, - TransactionTarget::Stored { runtime, .. } - | TransactionTarget::Session { runtime, .. } => { - matches!(runtime, TransactionRuntime::VmCasperV1) - && (!self.is_native_mint() && !self.is_native_auction()) - } - } - } - - /// Should this transaction start in the initiating accounts context? - pub fn is_account_session(&self) -> bool { - let target_is_stored_contract = matches!(self.target(), TransactionTarget::Stored { .. }); - !target_is_stored_contract - } - - /// Returns the approvals for this transaction. - pub fn approvals(&self) -> &BTreeSet { - &self.approvals - } - - /// Consumes `self`, returning a tuple of its constituent parts. - pub fn destructure( - self, - ) -> ( - TransactionV1Hash, - TransactionV1Header, - TransactionV1Body, - BTreeSet, - ) { - (self.hash, self.header, self.body, self.approvals) - } - - /// Adds a signature of this transaction's hash to its approvals. - pub fn sign(&mut self, secret_key: &SecretKey) { - let approval = Approval::create(&self.hash.into(), secret_key); - self.approvals.insert(approval); + self.payload.pricing_mode() } /// Returns the `ApprovalsHash` of this transaction's approvals. - pub fn compute_approvals_hash(&self) -> Result { + pub fn compute_approvals_hash(&self) -> Result { ApprovalsHash::compute(&self.approvals) } - /// Returns `true` if the serialized size of the transaction is not greater than - /// `max_transaction_size`. - #[cfg(any(all(feature = "std", feature = "testing"), test))] - fn is_valid_size( - &self, - max_transaction_size: u32, - ) -> Result<(), TransactionV1ExcessiveSizeError> { - let actual_transaction_size = self.serialized_length(); - if actual_transaction_size > max_transaction_size as usize { - return Err(TransactionV1ExcessiveSizeError { - max_transaction_size, - actual_transaction_size, - }); - } - Ok(()) - } - - /// Returns `Ok` if and only if this transaction's body hashes to the value of `body_hash()`, - /// and if this transaction's header hashes to the value claimed as the transaction hash. - pub fn has_valid_hash(&self) -> Result<(), InvalidTransactionV1> { - let body_hash = Digest::hash( - self.body - .to_bytes() - .unwrap_or_else(|error| panic!("should serialize body: {}", error)), - ); - if body_hash != *self.header.body_hash() { - debug!(?self, ?body_hash, "invalid transaction body hash"); - return Err(InvalidTransactionV1::InvalidBodyHash); - } - - let hash = TransactionV1Hash::new(Digest::hash( - self.header - .to_bytes() - .unwrap_or_else(|error| panic!("should serialize header: {}", error)), - )); - if hash != self.hash { - debug!(?self, ?hash, "invalid transaction hash"); - return Err(InvalidTransactionV1::InvalidTransactionHash); - } - Ok(()) - } - - /// Returns `Ok` if and only if: - /// * the transaction hash is correct (see [`TransactionV1::has_valid_hash`] for details) - /// * approvals are non empty, and - /// * all approvals are valid signatures of the signed hash - pub fn verify(&self) -> Result<(), InvalidTransactionV1> { - #[cfg(any(feature = "once_cell", test))] - return self.is_verified.get_or_init(|| self.do_verify()).clone(); - - #[cfg(not(any(feature = "once_cell", test)))] - self.do_verify() - } - - fn do_verify(&self) -> Result<(), InvalidTransactionV1> { - if self.approvals.is_empty() { - debug!(?self, "transaction has no approvals"); - return Err(InvalidTransactionV1::EmptyApprovals); - } - - self.has_valid_hash()?; - - for (index, approval) in self.approvals.iter().enumerate() { - if let Err(error) = crypto::verify(self.hash, approval.signature(), approval.signer()) { - debug!( - ?self, - "failed to verify transaction approval {}: {}", index, error - ); - return Err(InvalidTransactionV1::InvalidApproval { index, error }); - } - } - - Ok(()) - } - - /// Returns `Ok` if and only if: - /// * the chain_name is correct, - /// * the configured parameters are complied with at the given timestamp - #[cfg(any(all(feature = "std", feature = "testing"), test))] - pub fn is_config_compliant( - &self, - chainspec: &Chainspec, - timestamp_leeway: TimeDiff, - at: Timestamp, - ) -> Result<(), InvalidTransactionV1> { - let transaction_config = chainspec.transaction_config.clone(); - self.is_valid_size( - transaction_config - .transaction_v1_config - .get_max_serialized_length(self.body.transaction_category) as u32, - )?; - - let chain_name = chainspec.network_config.name.clone(); - - let header = self.header(); - if header.chain_name() != chain_name { - debug!( - transaction_hash = %self.hash(), - transaction_header = %header, - chain_name = %header.chain_name(), - "invalid chain identifier" - ); - return Err(InvalidTransactionV1::InvalidChainName { - expected: chain_name, - got: header.chain_name().to_string(), - }); - } - - let price_handling = chainspec.core_config.pricing_handling; - let price_mode = header.pricing_mode(); - - match price_mode { - PricingMode::Classic { .. } => { - if let PricingHandling::Classic = price_handling { - } else { - return Err(InvalidTransactionV1::InvalidPricingMode { - price_mode: price_mode.clone(), - }); - } - } - PricingMode::Fixed { .. } => { - if let PricingHandling::Fixed = price_handling { - } else { - return Err(InvalidTransactionV1::InvalidPricingMode { - price_mode: price_mode.clone(), - }); - } - } - PricingMode::Reserved { .. } => { - if !chainspec.core_config.allow_reservations { - // Currently Reserved isn't implemented and we should - // not be accepting transactions with this mode. - return Err(InvalidTransactionV1::InvalidPricingMode { - price_mode: price_mode.clone(), - }); - } - } - } - - let min_gas_price = chainspec.vacancy_config.min_gas_price; - let gas_price_tolerance = self.header.gas_price_tolerance(); - if gas_price_tolerance < min_gas_price { - return Err(InvalidTransactionV1::GasPriceToleranceTooLow { - min_gas_price_tolerance: min_gas_price, - provided_gas_price_tolerance: gas_price_tolerance, - }); - } - - header.is_valid(&transaction_config, timestamp_leeway, at, &self.hash)?; - - let max_associated_keys = chainspec.core_config.max_associated_keys; - - if self.approvals.len() > max_associated_keys as usize { - debug!( - transaction_hash = %self.hash(), - number_of_approvals = %self.approvals.len(), - max_associated_keys = %max_associated_keys, - "number of transaction approvals exceeds the limit" - ); - return Err(InvalidTransactionV1::ExcessiveApprovals { - got: self.approvals.len() as u32, - max_associated_keys, - }); - } - - let gas_limit = self.gas_limit(chainspec)?; - let block_gas_limit = Gas::new(U512::from(transaction_config.block_gas_limit)); - if gas_limit > block_gas_limit { - debug!( - amount = %gas_limit, - %block_gas_limit, - "transaction gas limit exceeds block gas limit" - ); - return Err(InvalidTransactionV1::ExceedsBlockGasLimit { - block_gas_limit: transaction_config.block_gas_limit, - got: Box::new(gas_limit.value()), - }); - } - - self.body.is_valid(&transaction_config) - } - - // This method is not intended to be used by third party crates. - // - // It is required to allow finalized approvals to be injected after reading a transaction from - // storage. #[doc(hidden)] pub fn with_approvals(mut self, approvals: BTreeSet) -> Self { self.approvals = approvals; self } + /// Used by the `TestTransactionV1Builder` to inject invalid approvals for testing purposes. + #[cfg(any(all(feature = "std", feature = "testing"), test))] + pub(super) fn apply_approvals(&mut self, approvals: Vec) { + self.approvals.extend(approvals); + } + /// Returns a random, valid but possibly expired transaction. /// /// Note that the [`TransactionV1Builder`] can be used to create a random transaction with @@ -515,20 +220,15 @@ impl TransactionV1 { timestamp: Option, ttl: Option, ) -> Self { - let transaction = TransactionV1Builder::new_random_with_category_and_timestamp_and_ttl( + let transaction_v1 = TransactionV1Builder::new_random_with_category_and_timestamp_and_ttl( rng, - TransactionCategory::Mint as u8, + MINT_LANE_ID, timestamp, ttl, ) .build() .unwrap(); - assert_eq!( - transaction.transaction_category(), - TransactionCategory::Mint as u8, - "Required mint, incorrect category" - ); - transaction + transaction_v1 } /// Returns a random transaction with "standard" category. @@ -543,17 +243,12 @@ impl TransactionV1 { ) -> Self { let transaction = TransactionV1Builder::new_random_with_category_and_timestamp_and_ttl( rng, - TransactionCategory::Large as u8, + LARGE_WASM_LANE_ID, timestamp, ttl, ) .build() .unwrap(); - assert_eq!( - transaction.transaction_category(), - TransactionCategory::Large as u8, - "Required large, incorrect category" - ); transaction } @@ -562,25 +257,19 @@ impl TransactionV1 { /// Note that the [`TransactionV1Builder`] can be used to create a random transaction with /// more specific values. #[cfg(any(all(feature = "std", feature = "testing"), test))] - pub fn random_install_upgrade( + pub fn random_auction( rng: &mut TestRng, timestamp: Option, ttl: Option, ) -> Self { - let transaction = TransactionV1Builder::new_random_with_category_and_timestamp_and_ttl( + TransactionV1Builder::new_random_with_category_and_timestamp_and_ttl( rng, - TransactionCategory::InstallUpgrade as u8, + AUCTION_LANE_ID, timestamp, ttl, ) .build() - .unwrap(); - assert_eq!( - transaction.transaction_category(), - TransactionCategory::InstallUpgrade as u8, - "Required install/upgrade, incorrect category" - ); - transaction + .unwrap() } /// Returns a random transaction with "install/upgrade" category. @@ -588,194 +277,137 @@ impl TransactionV1 { /// Note that the [`TransactionV1Builder`] can be used to create a random transaction with /// more specific values. #[cfg(any(all(feature = "std", feature = "testing"), test))] - pub fn random_auction( + pub fn random_install_upgrade( rng: &mut TestRng, timestamp: Option, ttl: Option, ) -> Self { let transaction = TransactionV1Builder::new_random_with_category_and_timestamp_and_ttl( rng, - TransactionCategory::Auction as u8, + INSTALL_UPGRADE_LANE_ID, timestamp, ttl, ) .build() .unwrap(); - assert_eq!( - transaction.transaction_category(), - TransactionCategory::Auction as u8, - "Required auction, incorrect category" - ); transaction } - /// Turns `self` into an invalid transaction by clearing the `chain_name`, invalidating the - /// transaction header hash. - #[cfg(any(all(feature = "std", feature = "testing"), test))] - pub fn invalidate(&mut self) { - self.header.invalidate(); + /// Returns result of attempting to deserailize a field from the amorphic `fields` container. + pub fn deserialize_field( + &self, + index: u16, + ) -> Result { + self.payload.deserialize_field(index) } - /// Used by the `TestTransactionV1Builder` to inject invalid approvals for testing purposes. - #[cfg(any(all(feature = "std", feature = "testing"), test))] - pub(super) fn apply_approvals(&mut self, approvals: Vec) { - self.approvals.extend(approvals); + /// Returns number of fields in the amorphic `fields` container. + pub fn number_of_fields(&self) -> usize { + self.payload.number_of_fields() } -} -#[cfg(any(feature = "std", test))] -impl GasLimited for TransactionV1 { - type Error = InvalidTransactionV1; + /// Checks if the declared hash of the transaction matches calculated hash. + pub fn has_valid_hash(&self) -> Result<(), InvalidTransactionV1> { + let computed_hash = Digest::hash( + self.payload + .to_bytes() + .unwrap_or_else(|error| panic!("should serialize body: {}", error)), + ); + if TransactionV1Hash::new(computed_hash) != self.hash { + debug!(?self, ?computed_hash, "invalid transaction hash"); + return Err(InvalidTransactionV1::InvalidTransactionHash); + } + Ok(()) + } - fn gas_cost(&self, chainspec: &Chainspec, gas_price: u8) -> Result { - let gas_limit = self.gas_limit(chainspec)?; - let motes = match self.header().pricing_mode() { - PricingMode::Classic { .. } | PricingMode::Fixed { .. } => { - Motes::from_gas(gas_limit, gas_price) - .ok_or(InvalidTransactionV1::UnableToCalculateGasCost)? - } - PricingMode::Reserved { .. } => { - Motes::zero() // prepaid - } - }; - Ok(motes) + /// Returns `Ok` if and only if: + /// * the transaction hash is correct (see [`TransactionV1::has_valid_hash`] for details) + /// * approvals are non empty, and + /// * all approvals are valid signatures of the signed hash + pub fn verify(&self) -> Result<(), InvalidTransactionV1> { + #[cfg(any(feature = "once_cell", test))] + return self.is_verified.get_or_init(|| self.do_verify()).clone(); + + #[cfg(not(any(feature = "once_cell", test)))] + self.do_verify() } - fn gas_limit(&self, chainspec: &Chainspec) -> Result { - let costs = chainspec.system_costs_config; - let gas = match self.header().pricing_mode() { - PricingMode::Classic { payment_amount, .. } => Gas::new(*payment_amount), - PricingMode::Fixed { .. } => { - let computation_limit = { - if self.is_native_mint() { - // Because we currently only support one native mint interaction, - // native transfer, we can short circuit to return that value. - // However if other direct mint interactions are supported - // in the future (such as the upcoming burn feature), - // this logic will need to be expanded to self.mint_costs().field? - // for the value for each verb...see how auction is set up below. - costs.mint_costs().transfer as u64 - } else if self.is_native_auction() { - let entry_point = self.body().entry_point(); - let amount = match entry_point { - TransactionEntryPoint::Call => { - return Err(InvalidTransactionV1::EntryPointCannotBeCall) - } - TransactionEntryPoint::Custom(_) | TransactionEntryPoint::Transfer => { - return Err(InvalidTransactionV1::EntryPointCannotBeCustom { - entry_point: entry_point.clone(), - }); - } - TransactionEntryPoint::AddBid | TransactionEntryPoint::ActivateBid => { - costs.auction_costs().add_bid.into() - } - TransactionEntryPoint::WithdrawBid => { - costs.auction_costs().withdraw_bid.into() - } - TransactionEntryPoint::Delegate => { - costs.auction_costs().delegate.into() - } - TransactionEntryPoint::Undelegate => { - costs.auction_costs().undelegate.into() - } - TransactionEntryPoint::Redelegate => { - costs.auction_costs().redelegate.into() - } - TransactionEntryPoint::ChangeBidPublicKey => { - costs.auction_costs().change_bid_public_key - } - TransactionEntryPoint::AddReservations => { - costs.auction_costs().add_reservations.into() - } - TransactionEntryPoint::CancelReservations => { - costs.auction_costs().cancel_reservations.into() - } - }; - amount - } else { - chainspec.get_max_gas_limit_by_category(self.body.transaction_category) - } - }; - Gas::new(U512::from(computation_limit)) - } - PricingMode::Reserved { receipt } => { - return Err(InvalidTransactionV1::InvalidPricingMode { - price_mode: PricingMode::Reserved { receipt: *receipt }, - }); + fn do_verify(&self) -> Result<(), InvalidTransactionV1> { + if self.approvals.is_empty() { + debug!(?self, "transaction has no approvals"); + return Err(InvalidTransactionV1::EmptyApprovals); + } + + self.has_valid_hash()?; + + for (index, approval) in self.approvals.iter().enumerate() { + if let Err(error) = crypto::verify(self.hash, approval.signature(), approval.signer()) { + debug!( + ?self, + "failed to verify transaction approval {}: {}", index, error + ); + return Err(InvalidTransactionV1::InvalidApproval { index, error }); } - }; - Ok(gas) + } + + Ok(()) } - fn gas_price_tolerance(&self) -> Result { - Ok(self.header.gas_price_tolerance()) + /// Returns the hash of the transaction's payload. + pub fn payload_hash(&self) -> Result { + let bytes = self + .payload + .fields() + .to_bytes() + .map_err(|_| InvalidTransactionV1::CannotCalculateFieldsHash)?; + Ok(Digest::hash(bytes)) } -} -impl hash::Hash for TransactionV1 { - fn hash(&self, state: &mut H) { - // Destructure to make sure we don't accidentally omit fields. - let TransactionV1 { - serialization_version, - hash, - header, - body, - approvals, - #[cfg(any(feature = "once_cell", test))] - is_verified: _, - } = self; - serialization_version.hash(state); - hash.hash(state); - header.hash(state); - body.hash(state); - approvals.hash(state); + fn serialized_field_lengths(&self) -> Vec { + vec![ + self.hash.serialized_length(), + self.payload.serialized_length(), + self.approvals.serialized_length(), + ] } -} -impl PartialEq for TransactionV1 { - fn eq(&self, other: &TransactionV1) -> bool { - // Destructure to make sure we don't accidentally omit fields. - let TransactionV1 { - serialization_version, - hash, - header, - body, - approvals, - #[cfg(any(feature = "once_cell", test))] - is_verified: _, - } = self; - *serialization_version == other.serialization_version - && *hash == other.hash - && *header == other.header - && *body == other.body - && *approvals == other.approvals + /// Turns `self` into an invalid `TransactionV1` by clearing the `chain_name`, invalidating the + /// transaction hash + #[cfg(any(all(feature = "std", feature = "testing"), test))] + pub fn invalidate(&mut self) { + self.payload.invalidate(); } -} -impl Ord for TransactionV1 { - fn cmp(&self, other: &TransactionV1) -> cmp::Ordering { - // Destructure to make sure we don't accidentally omit fields. - let TransactionV1 { - serialization_version, - hash, - header, - body, - approvals, - #[cfg(any(feature = "once_cell", test))] - is_verified: _, - } = self; - serialization_version - .cmp(&other.serialization_version) - .then_with(|| hash.cmp(&other.hash)) - .then_with(|| header.cmp(&other.header)) - .then_with(|| body.cmp(&other.body)) - .then_with(|| approvals.cmp(&other.approvals)) + #[cfg(any(all(feature = "std", feature = "testing"), test))] + pub(crate) fn get_transaction_target(&self) -> Result { + self.deserialize_field::(TARGET_MAP_KEY) + .map_err(|error| InvalidTransactionV1::CouldNotDeserializeField { error }) } -} -impl PartialOrd for TransactionV1 { - fn partial_cmp(&self, other: &TransactionV1) -> Option { - Some(self.cmp(other)) + #[cfg(any(all(feature = "std", feature = "testing"), test))] + pub(crate) fn get_transaction_entry_point( + &self, + ) -> Result { + self.deserialize_field::(ENTRY_POINT_MAP_KEY) + .map_err(|error| InvalidTransactionV1::CouldNotDeserializeField { error }) + } + + /// Returns the gas price tolerance for the given transaction. + pub fn gas_price_tolerance(&self) -> u8 { + match self.pricing_mode() { + PricingMode::Classic { + gas_price_tolerance, + .. + } => *gas_price_tolerance, + PricingMode::Fixed { + gas_price_tolerance, + .. + } => *gas_price_tolerance, + PricingMode::Reserved { .. } => { + // TODO: Change this when reserve gets implemented. + 0u8 + } + } } } @@ -783,11 +415,9 @@ impl ToBytes for TransactionV1 { fn to_bytes(&self) -> Result, crate::bytesrepr::Error> { let expected_payload_sizes = self.serialized_field_lengths(); CalltableSerializationEnvelopeBuilder::new(expected_payload_sizes)? - .add_field(SERIALIZATION_VERSION_INDEX, &self.serialization_version)? - .add_field(HASH_FIELD_META_INDEX, &self.hash)? - .add_field(HEADER_FIELD_META_INDEX, &self.header)? - .add_field(BODY_FIELD_META_INDEX, &self.body)? - .add_field(APPROVALS_FIELD_META_INDEX, &self.approvals)? + .add_field(HASH_FIELD_INDEX, &self.hash)? + .add_field(PAYLOAD_FIELD_INDEX, &self.payload)? + .add_field(APPROVALS_FIELD_INDEX, &self.approvals)? .binary_payload_bytes() } @@ -798,39 +428,26 @@ impl ToBytes for TransactionV1 { impl FromBytes for TransactionV1 { fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), Error> { - let (binary_payload, remainder) = CalltableSerializationEnvelope::from_bytes(5, bytes)?; - let window = binary_payload.start_consuming()?.ok_or(Formatting)?; - window.verify_index(SERIALIZATION_VERSION_INDEX)?; - - let (serialization_version, window) = window.deserialize_and_maybe_next::()?; - if serialization_version != TRANSACTION_V1_SERIALIZATION_VERSION { - return Err(Formatting); - } - let window = window.ok_or(Formatting)?; - window.verify_index(HASH_FIELD_META_INDEX)?; + let (binary_payload, remainder) = CalltableSerializationEnvelope::from_bytes(3, bytes)?; + let window = binary_payload.start_consuming()?.ok_or(Error::Formatting)?; + window.verify_index(HASH_FIELD_INDEX)?; let (hash, window) = window.deserialize_and_maybe_next::()?; - let window = window.ok_or(Formatting)?; - window.verify_index(HEADER_FIELD_META_INDEX)?; - let (header, window) = window.deserialize_and_maybe_next::()?; - let window = window.ok_or(Formatting)?; - window.verify_index(BODY_FIELD_META_INDEX)?; - let (body, window) = window.deserialize_and_maybe_next::()?; - let window = window.ok_or(Formatting)?; - window.verify_index(APPROVALS_FIELD_META_INDEX)?; + let window = window.ok_or(Error::Formatting)?; + window.verify_index(PAYLOAD_FIELD_INDEX)?; + let (payload, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(Error::Formatting)?; + window.verify_index(APPROVALS_FIELD_INDEX)?; let (approvals, window) = window.deserialize_and_maybe_next::>()?; if window.is_some() { - return Err(Formatting); + return Err(Error::Formatting); } let from_bytes = TransactionV1 { - serialization_version, hash, - header, - body, + payload, approvals, #[cfg(any(feature = "once_cell", test))] is_verified: OnceCell::new(), }; - Ok((from_bytes, remainder)) } } @@ -840,565 +457,61 @@ impl Display for TransactionV1 { write!( formatter, "transaction-v1[{}, {}, approvals: {}]", - self.header, - self.body, + self.hash, + self.payload, DisplayIter::new(self.approvals.iter()) ) } } -#[cfg(test)] -mod tests { - use std::time::Duration; - - use crate::bytesrepr; - - use super::*; - - const MAX_ASSOCIATED_KEYS: u32 = 5; - - #[test] - fn json_roundtrip() { - let rng = &mut TestRng::new(); - let transaction = TransactionV1::random(rng); - let json_string = serde_json::to_string_pretty(&transaction).unwrap(); - let decoded = serde_json::from_str(&json_string).unwrap(); - assert_eq!(transaction, decoded); - } - - #[test] - fn bincode_roundtrip() { - let rng = &mut TestRng::new(); - let transaction = TransactionV1::random(rng); - let serialized = bincode::serialize(&transaction).unwrap(); - let deserialized = bincode::deserialize(&serialized).unwrap(); - assert_eq!(transaction, deserialized); - } - - #[test] - fn bytesrepr_roundtrip() { - let rng = &mut TestRng::new(); - let transaction = TransactionV1::random(rng); - bytesrepr::test_serialization_roundtrip(transaction.header()); - bytesrepr::test_serialization_roundtrip(&transaction); - } - - #[test] - fn is_valid() { - let rng = &mut TestRng::new(); - let transaction = TransactionV1::random(rng); - assert_eq!( - transaction.is_verified.get(), - None, - "is_verified should initially be None" - ); - transaction.verify().expect("should verify"); - assert_eq!( - transaction.is_verified.get(), - Some(&Ok(())), - "is_verified should be true" - ); - } - - fn check_is_not_valid( - invalid_transaction: TransactionV1, - expected_error: InvalidTransactionV1, - ) { - assert!( - invalid_transaction.is_verified.get().is_none(), - "is_verified should initially be None" - ); - let actual_error = invalid_transaction.verify().unwrap_err(); - - // Ignore the `error_msg` field of `InvalidApproval` when comparing to expected error, as - // this makes the test too fragile. Otherwise expect the actual error should exactly match - // the expected error. - match expected_error { - InvalidTransactionV1::InvalidApproval { - index: expected_index, - .. - } => match actual_error { - InvalidTransactionV1::InvalidApproval { - index: actual_index, - .. - } => { - assert_eq!(actual_index, expected_index); - } - _ => panic!("expected {}, got: {}", expected_error, actual_error), - }, - _ => { - assert_eq!(actual_error, expected_error,); - } - } - - // The actual error should have been lazily initialized correctly. - assert_eq!( - invalid_transaction.is_verified.get(), - Some(&Err(actual_error)), - "is_verified should now be Some" - ); - } - - #[test] - fn not_valid_due_to_invalid_transaction_hash() { - let rng = &mut TestRng::new(); - let mut transaction = TransactionV1::random(rng); - - transaction.invalidate(); - check_is_not_valid(transaction, InvalidTransactionV1::InvalidTransactionHash); - } - - #[test] - fn not_valid_due_to_empty_approvals() { - let rng = &mut TestRng::new(); - let transaction = TransactionV1Builder::new_random(rng) - .with_no_secret_key() - .build() - .unwrap(); - assert!(transaction.approvals.is_empty()); - check_is_not_valid(transaction, InvalidTransactionV1::EmptyApprovals) - } - - #[test] - fn not_valid_due_to_invalid_approval() { - let rng = &mut TestRng::new(); - let transaction = TransactionV1Builder::new_random(rng) - .with_invalid_approval(rng) - .build() - .unwrap(); - - // The expected index for the invalid approval will be the first index at which there is an - // approval where the signer is not the account holder. - let account_holder = match transaction.initiator_addr() { - InitiatorAddr::PublicKey(public_key) => public_key.clone(), - InitiatorAddr::AccountHash(_) => unreachable!(), - }; - let expected_index = transaction - .approvals - .iter() - .enumerate() - .find(|(_, approval)| approval.signer() != &account_holder) - .map(|(index, _)| index) - .unwrap(); - check_is_not_valid( - transaction, - InvalidTransactionV1::InvalidApproval { - index: expected_index, - error: crypto::Error::SignatureError, // This field is ignored in the check. - }, - ); - } - - #[test] - fn is_config_compliant() { - let rng = &mut TestRng::new(); - let chain_name = "net-1"; - let transaction = TransactionV1Builder::new_random_with_category_and_timestamp_and_ttl( - rng, - TransactionCategory::Large as u8, - None, - None, - ) - .with_chain_name(chain_name) - .build() - .unwrap(); - let current_timestamp = transaction.timestamp(); - let chainspec = { - let mut ret = Chainspec::default(); - ret.network_config.name = chain_name.to_string(); - ret - }; - transaction - .is_config_compliant(&chainspec, TimeDiff::default(), current_timestamp) - .expect("should be acceptable"); - } - - #[test] - fn not_acceptable_due_to_invalid_chain_name() { - let rng = &mut TestRng::new(); - let expected_chain_name = "net-1"; - let wrong_chain_name = "net-2"; - let transaction = TransactionV1Builder::new_random(rng) - .with_chain_name(wrong_chain_name) - .build() - .unwrap(); - - let expected_error = InvalidTransactionV1::InvalidChainName { - expected: expected_chain_name.to_string(), - got: wrong_chain_name.to_string(), - }; - - let current_timestamp = transaction.timestamp(); - let chainspec = { - let mut ret = Chainspec::default(); - ret.network_config.name = expected_chain_name.to_string(); - ret - }; - assert_eq!( - transaction.is_config_compliant(&chainspec, TimeDiff::default(), current_timestamp), - Err(expected_error) - ); - assert!( - transaction.is_verified.get().is_none(), - "transaction should not have run expensive `is_verified` call" - ); - } - - #[test] - fn not_acceptable_due_to_excessive_ttl() { - let rng = &mut TestRng::new(); - let chain_name = "net-1"; - let transaction_config = TransactionConfig::default(); - let ttl = transaction_config.max_ttl + TimeDiff::from(Duration::from_secs(1)); - let transaction = TransactionV1Builder::new_random(rng) - .with_ttl(ttl) - .with_chain_name(chain_name) - .build() - .unwrap(); - - let expected_error = InvalidTransactionV1::ExcessiveTimeToLive { - max_ttl: transaction_config.max_ttl, - got: ttl, - }; - - let current_timestamp = transaction.timestamp(); - let chainspec = { - let mut ret = Chainspec::default(); - ret.network_config.name = chain_name.to_string(); - ret - }; - assert_eq!( - transaction.is_config_compliant(&chainspec, TimeDiff::default(), current_timestamp), - Err(expected_error) - ); - assert!( - transaction.is_verified.get().is_none(), - "transaction should not have run expensive `is_verified` call" - ); - } - - #[test] - fn not_acceptable_due_to_timestamp_in_future() { - let rng = &mut TestRng::new(); - let chain_name = "net-1"; - let leeway = TimeDiff::from_seconds(2); - - let transaction = TransactionV1Builder::new_random(rng) - .with_chain_name(chain_name) - .build() - .unwrap(); - let current_timestamp = transaction.timestamp() - leeway - TimeDiff::from_seconds(1); - - let expected_error = InvalidTransactionV1::TimestampInFuture { - validation_timestamp: current_timestamp, - timestamp_leeway: leeway, - got: transaction.timestamp(), - }; - - let chainspec = { - let mut ret = Chainspec::default(); - ret.network_config.name = chain_name.to_string(); - ret - }; - - assert_eq!( - transaction.is_config_compliant(&chainspec, leeway, current_timestamp), - Err(expected_error) - ); - assert!( - transaction.is_verified.get().is_none(), - "transaction should not have run expensive `is_verified` call" - ); - } - - #[test] - fn not_acceptable_due_to_excessive_approvals() { - let rng = &mut TestRng::new(); - let chain_name = "net-1"; - let mut transaction = TransactionV1Builder::new_random(rng) - .with_chain_name(chain_name) - .build() - .unwrap(); - - for _ in 0..MAX_ASSOCIATED_KEYS { - transaction.sign(&SecretKey::random(rng)); - } - - let current_timestamp = transaction.timestamp(); - - let expected_error = InvalidTransactionV1::ExcessiveApprovals { - got: MAX_ASSOCIATED_KEYS + 1, - max_associated_keys: MAX_ASSOCIATED_KEYS, - }; - - let chainspec = { - let mut ret = Chainspec::default(); - ret.network_config.name = chain_name.to_string(); - ret.core_config.max_associated_keys = MAX_ASSOCIATED_KEYS; - ret - }; - - assert_eq!( - transaction.is_config_compliant(&chainspec, TimeDiff::default(), current_timestamp), - Err(expected_error) - ); - assert!( - transaction.is_verified.get().is_none(), - "transaction should not have run expensive `is_verified` call" - ); - } - - #[test] - fn not_acceptable_due_to_invalid_pricing_modes() { - let rng = &mut TestRng::new(); - let chain_name = "net-1"; - - let reserved_mode = PricingMode::Reserved { - receipt: Default::default(), - }; - - let reserved_transaction = TransactionV1Builder::new_random(rng) - .with_chain_name(chain_name) - .with_pricing_mode(reserved_mode.clone()) - .build() - .expect("must be able to create a reserved transaction"); - - let chainspec = { - let mut ret = Chainspec::default(); - ret.network_config.name = chain_name.to_string(); - ret - }; - - let current_timestamp = reserved_transaction.timestamp(); - let expected_error = InvalidTransactionV1::InvalidPricingMode { - price_mode: reserved_mode, - }; - assert_eq!( - reserved_transaction.is_config_compliant( - &chainspec, - TimeDiff::default(), - current_timestamp, - ), - Err(expected_error) - ); - assert!( - reserved_transaction.is_verified.get().is_none(), - "transaction should not have run expensive `is_verified` call" - ); - - let fixed_mode_transaction = TransactionV1Builder::new_random(rng) - .with_chain_name(chain_name) - .with_pricing_mode(PricingMode::Fixed { - gas_price_tolerance: 1u8, - }) - .build() - .expect("must create fixed mode transaction"); - - let fixed_handling_chainspec = { - let mut ret = Chainspec::default(); - ret.network_config.name = chain_name.to_string(); - ret.core_config.pricing_handling = PricingHandling::Fixed; - ret - }; - - let classic_handling_chainspec = { - let mut ret = Chainspec::default(); - ret.network_config.name = chain_name.to_string(); - ret.core_config.pricing_handling = PricingHandling::Classic; - ret - }; - - let current_timestamp = fixed_mode_transaction.timestamp(); - let expected_error = InvalidTransactionV1::InvalidPricingMode { - price_mode: fixed_mode_transaction.pricing_mode().clone(), - }; - - assert_eq!( - fixed_mode_transaction.is_config_compliant( - &classic_handling_chainspec, - TimeDiff::default(), - current_timestamp, - ), - Err(expected_error) - ); - assert!( - fixed_mode_transaction.is_verified.get().is_none(), - "transaction should not have run expensive `is_verified` call" - ); - - assert!(fixed_mode_transaction - .is_config_compliant( - &fixed_handling_chainspec, - TimeDiff::default(), - current_timestamp, - ) - .is_ok()); - - let classic_mode_transaction = TransactionV1Builder::new_random(rng) - .with_chain_name(chain_name) - .with_pricing_mode(PricingMode::Classic { - payment_amount: 100000, - gas_price_tolerance: 1, - standard_payment: true, - }) - .build() - .expect("must create classic transaction"); - - let current_timestamp = classic_mode_transaction.timestamp(); - let expected_error = InvalidTransactionV1::InvalidPricingMode { - price_mode: classic_mode_transaction.pricing_mode().clone(), - }; - - assert_eq!( - classic_mode_transaction.is_config_compliant( - &fixed_handling_chainspec, - TimeDiff::default(), - current_timestamp, - ), - Err(expected_error) - ); - assert!( - classic_mode_transaction.is_verified.get().is_none(), - "transaction should not have run expensive `is_verified` call" - ); - - assert!(classic_mode_transaction - .is_config_compliant( - &classic_handling_chainspec, - TimeDiff::default(), - current_timestamp, - ) - .is_ok()); - } - - #[test] - fn should_use_payment_amount_for_classic_payment() { - let payment_amount = 500u64; - let mut chainspec = Chainspec::default(); - let chain_name = "net-1"; - chainspec - .with_chain_name(chain_name.to_string()) - .with_pricing_handling(PricingHandling::Classic); - - let rng = &mut TestRng::new(); - let builder = TransactionV1Builder::new_random(rng) - .with_chain_name(chain_name) - .with_pricing_mode(PricingMode::Classic { - payment_amount, - gas_price_tolerance: 1, - standard_payment: true, - }); - let transaction = builder.build().expect("should build"); - let mut gas_price = 1; - let cost = transaction - .gas_cost(&chainspec, gas_price) - .expect("should cost") - .value(); - assert_eq!( - cost, - U512::from(payment_amount), - "in classic pricing, the user selected amount should be the cost if gas price is 1" - ); - gas_price += 1; - let cost = transaction - .gas_cost(&chainspec, gas_price) - .expect("should cost") - .value(); - assert_eq!( - cost, - U512::from(payment_amount) * gas_price, - "in classic pricing, the cost should == user selected amount * gas_price" - ); +impl hash::Hash for TransactionV1 { + fn hash(&self, state: &mut H) { + // Destructure to make sure we don't accidentally omit fields. + let TransactionV1 { + hash, + payload, + approvals, + #[cfg(any(feature = "once_cell", test))] + is_verified: _, + } = self; + hash.hash(state); + payload.hash(state); + approvals.hash(state); } +} - #[test] - fn should_use_cost_table_for_fixed_payment() { - let mut chainspec = Chainspec::default(); - let chain_name = "net-1"; - chainspec - .with_chain_name(chain_name.to_string()) - .with_pricing_handling(PricingHandling::Fixed); - - let rng = &mut TestRng::new(); - let builder = TransactionV1Builder::new_random(rng) - .with_chain_name(chain_name) - .with_pricing_mode(PricingMode::Fixed { - gas_price_tolerance: 5, - }); - let transaction = builder.build().expect("should build"); - let mut gas_price = 1; - let limit = transaction - .gas_limit(&chainspec) - .expect("should limit") - .value(); - let cost = transaction - .gas_cost(&chainspec, gas_price) - .expect("should cost") - .value(); - assert_eq!( - cost, limit, - "in fixed pricing, the cost & limit should == if gas price is 1" - ); - gas_price += 1; - let cost = transaction - .gas_cost(&chainspec, gas_price) - .expect("should cost") - .value(); - assert_eq!( - cost, - limit * gas_price, - "in fixed pricing, the cost should == limit * gas_price" - ); +impl PartialEq for TransactionV1 { + fn eq(&self, other: &TransactionV1) -> bool { + // Destructure to make sure we don't accidentally omit fields. + let TransactionV1 { + hash, + payload, + approvals, + #[cfg(any(feature = "once_cell", test))] + is_verified: _, + } = self; + *hash == other.hash && *payload == other.payload && *approvals == other.approvals } } -#[cfg(test)] -mod proptests { - use super::gens::v1_transaction_arb; - use crate::bytesrepr::test_serialization_roundtrip; - use proptest::prelude::*; - - proptest! { - #[test] - fn bytesrepr_roundtrip(v1_transaction in v1_transaction_arb()) { - test_serialization_roundtrip(&v1_transaction); - } +impl Ord for TransactionV1 { + fn cmp(&self, other: &TransactionV1) -> cmp::Ordering { + // Destructure to make sure we don't accidentally omit fields. + let TransactionV1 { + hash, + payload, + approvals, + #[cfg(any(feature = "once_cell", test))] + is_verified: _, + } = self; + hash.cmp(&other.hash) + .then_with(|| payload.cmp(&other.payload)) + .then_with(|| approvals.cmp(&other.approvals)) } } -#[cfg(test)] -pub(crate) mod gens { - use super::*; - use crate::{gens::*, PublicKey}; - use crypto::gens::secret_key_arb_no_system; - use proptest::prelude::*; - - pub fn v1_transaction_arb() -> impl Strategy { - ( - any::(), - any::(), - any::(), - v1_transaction_body_arb(), - pricing_mode_arb(), - secret_key_arb_no_system(), - ) - .prop_map( - |(chain_name, timestamp, ttl, body, pricing_mode, secret_key)| { - let public_key = PublicKey::from(&secret_key); - let initiator_addr = InitiatorAddr::PublicKey(public_key); - let initiator_addr_with_secret = InitiatorAddrAndSecretKey::Both { - initiator_addr, - secret_key: &secret_key, - }; - TransactionV1::build( - chain_name, - Timestamp::from(timestamp), - TimeDiff::from_seconds(ttl), - body, - pricing_mode, - initiator_addr_with_secret, - ) - }, - ) +impl PartialOrd for TransactionV1 { + fn partial_cmp(&self, other: &TransactionV1) -> Option { + Some(self.cmp(other)) } } diff --git a/types/src/transaction/transaction_v1/transaction_v1_body/arg_handling.rs b/types/src/transaction/transaction_v1/arg_handling.rs similarity index 94% rename from types/src/transaction/transaction_v1/transaction_v1_body/arg_handling.rs rename to types/src/transaction/transaction_v1/arg_handling.rs index a136ce3cae..2936b6ee1b 100644 --- a/types/src/transaction/transaction_v1/transaction_v1_body/arg_handling.rs +++ b/types/src/transaction/transaction_v1/arg_handling.rs @@ -1,14 +1,19 @@ +//! Collection of helper functions and structures to reason about amorphic RuntimeArgs. use core::marker::PhantomData; +#[cfg(any(all(feature = "std", feature = "testing"), test))] use tracing::debug; #[cfg(any(all(feature = "std", feature = "testing"), test))] -use crate::{account::AccountHash, system::auction::ARG_VALIDATOR, CLType}; use crate::{ - bytesrepr::{FromBytes, ToBytes}, - CLTyped, CLValue, CLValueError, InvalidTransactionV1, PublicKey, RuntimeArgs, TransferTarget, - URef, U512, + account::AccountHash, bytesrepr::FromBytes, system::auction::ARG_VALIDATOR, CLType, CLValue, + InvalidTransactionV1, +}; +use crate::{ + bytesrepr::ToBytes, CLTyped, CLValueError, PublicKey, RuntimeArgs, TransferTarget, URef, U512, }; +#[cfg(any(all(feature = "std", feature = "testing"), test))] +use alloc::string::ToString; const TRANSFER_ARG_AMOUNT: RequiredArg = RequiredArg::new("amount"); const TRANSFER_ARG_SOURCE: OptionalArg = OptionalArg::new("source"); @@ -42,7 +47,9 @@ const REDELEGATE_ARG_NEW_VALIDATOR: RequiredArg = RequiredArg::new("n #[cfg(any(all(feature = "std", feature = "testing"), test))] const ACTIVATE_BID_ARG_VALIDATOR: RequiredArg = RequiredArg::new(ARG_VALIDATOR); +#[cfg(any(all(feature = "std", feature = "testing"), test))] const CHANGE_BID_PUBLIC_KEY_ARG_PUBLIC_KEY: RequiredArg = RequiredArg::new("public_key"); +#[cfg(any(all(feature = "std", feature = "testing"), test))] const CHANGE_BID_PUBLIC_KEY_ARG_NEW_PUBLIC_KEY: RequiredArg = RequiredArg::new("new_public_key"); @@ -59,6 +66,7 @@ impl RequiredArg { } } + #[cfg(any(all(feature = "std", feature = "testing"), test))] fn get(&self, args: &RuntimeArgs) -> Result where T: CLTyped + FromBytes, @@ -114,6 +122,7 @@ impl OptionalArg { } } +#[cfg(any(all(feature = "std", feature = "testing"), test))] fn parse_cl_value( cl_value: &CLValue, arg_name: &str, @@ -136,10 +145,7 @@ fn parse_cl_value( } /// Creates a `RuntimeArgs` suitable for use in a transfer transaction. -pub(in crate::transaction::transaction_v1) fn new_transfer_args< - A: Into, - T: Into, ->( +pub fn new_transfer_args, T: Into>( amount: A, maybe_source: Option, target: T, @@ -165,7 +171,7 @@ pub(in crate::transaction::transaction_v1) fn new_transfer_args< /// Checks the given `RuntimeArgs` are suitable for use in a transfer transaction. #[cfg(any(all(feature = "std", feature = "testing"), test))] -pub(in crate::transaction::transaction_v1) fn has_valid_transfer_args( +pub fn has_valid_transfer_args( args: &RuntimeArgs, native_transfer_minimum_motes: u64, ) -> Result<(), InvalidTransactionV1> { @@ -221,7 +227,7 @@ pub(in crate::transaction::transaction_v1) fn has_valid_transfer_args( } /// Creates a `RuntimeArgs` suitable for use in an add_bid transaction. -pub(in crate::transaction::transaction_v1) fn new_add_bid_args>( +pub fn new_add_bid_args>( public_key: PublicKey, delegation_rate: u8, amount: A, @@ -239,9 +245,7 @@ pub(in crate::transaction::transaction_v1) fn new_add_bid_args>( /// Checks the given `RuntimeArgs` are suitable for use in an add_bid transaction. #[cfg(any(all(feature = "std", feature = "testing"), test))] -pub(in crate::transaction::transaction_v1) fn has_valid_add_bid_args( - args: &RuntimeArgs, -) -> Result<(), InvalidTransactionV1> { +pub fn has_valid_add_bid_args(args: &RuntimeArgs) -> Result<(), InvalidTransactionV1> { let _public_key = ADD_BID_ARG_PUBLIC_KEY.get(args)?; let _delegation_rate = ADD_BID_ARG_DELEGATION_RATE.get(args)?; let _amount = ADD_BID_ARG_AMOUNT.get(args)?; @@ -249,7 +253,7 @@ pub(in crate::transaction::transaction_v1) fn has_valid_add_bid_args( } /// Creates a `RuntimeArgs` suitable for use in a withdraw_bid transaction. -pub(in crate::transaction::transaction_v1) fn new_withdraw_bid_args>( +pub fn new_withdraw_bid_args>( public_key: PublicKey, amount: A, ) -> Result { @@ -261,16 +265,14 @@ pub(in crate::transaction::transaction_v1) fn new_withdraw_bid_args Result<(), InvalidTransactionV1> { +pub fn has_valid_withdraw_bid_args(args: &RuntimeArgs) -> Result<(), InvalidTransactionV1> { let _public_key = WITHDRAW_BID_ARG_PUBLIC_KEY.get(args)?; let _amount = WITHDRAW_BID_ARG_AMOUNT.get(args)?; Ok(()) } /// Creates a `RuntimeArgs` suitable for use in a delegate transaction. -pub(in crate::transaction::transaction_v1) fn new_delegate_args>( +pub fn new_delegate_args>( delegator: PublicKey, validator: PublicKey, amount: A, @@ -284,9 +286,7 @@ pub(in crate::transaction::transaction_v1) fn new_delegate_args>( /// Checks the given `RuntimeArgs` are suitable for use in a delegate transaction. #[cfg(any(all(feature = "std", feature = "testing"), test))] -pub(in crate::transaction::transaction_v1) fn has_valid_delegate_args( - args: &RuntimeArgs, -) -> Result<(), InvalidTransactionV1> { +pub fn has_valid_delegate_args(args: &RuntimeArgs) -> Result<(), InvalidTransactionV1> { let _delegator = DELEGATE_ARG_DELEGATOR.get(args)?; let _validator = DELEGATE_ARG_VALIDATOR.get(args)?; let _amount = DELEGATE_ARG_AMOUNT.get(args)?; @@ -294,7 +294,7 @@ pub(in crate::transaction::transaction_v1) fn has_valid_delegate_args( } /// Creates a `RuntimeArgs` suitable for use in an undelegate transaction. -pub(in crate::transaction::transaction_v1) fn new_undelegate_args>( +pub fn new_undelegate_args>( delegator: PublicKey, validator: PublicKey, amount: A, @@ -308,9 +308,7 @@ pub(in crate::transaction::transaction_v1) fn new_undelegate_args> /// Checks the given `RuntimeArgs` are suitable for use in an undelegate transaction. #[cfg(any(all(feature = "std", feature = "testing"), test))] -pub(in crate::transaction::transaction_v1) fn has_valid_undelegate_args( - args: &RuntimeArgs, -) -> Result<(), InvalidTransactionV1> { +pub fn has_valid_undelegate_args(args: &RuntimeArgs) -> Result<(), InvalidTransactionV1> { let _delegator = UNDELEGATE_ARG_DELEGATOR.get(args)?; let _validator = UNDELEGATE_ARG_VALIDATOR.get(args)?; let _amount = UNDELEGATE_ARG_AMOUNT.get(args)?; @@ -318,7 +316,7 @@ pub(in crate::transaction::transaction_v1) fn has_valid_undelegate_args( } /// Creates a `RuntimeArgs` suitable for use in a redelegate transaction. -pub(in crate::transaction::transaction_v1) fn new_redelegate_args>( +pub fn new_redelegate_args>( delegator: PublicKey, validator: PublicKey, amount: A, @@ -334,9 +332,7 @@ pub(in crate::transaction::transaction_v1) fn new_redelegate_args> /// Checks the given `RuntimeArgs` are suitable for use in a redelegate transaction. #[cfg(any(all(feature = "std", feature = "testing"), test))] -pub(in crate::transaction::transaction_v1) fn has_valid_redelegate_args( - args: &RuntimeArgs, -) -> Result<(), InvalidTransactionV1> { +pub fn has_valid_redelegate_args(args: &RuntimeArgs) -> Result<(), InvalidTransactionV1> { let _delegator = REDELEGATE_ARG_DELEGATOR.get(args)?; let _validator = REDELEGATE_ARG_VALIDATOR.get(args)?; let _amount = REDELEGATE_ARG_AMOUNT.get(args)?; @@ -346,16 +342,15 @@ pub(in crate::transaction::transaction_v1) fn has_valid_redelegate_args( /// Checks the given `RuntimeArgs` are suitable for use in an activate bid transaction. #[cfg(any(all(feature = "std", feature = "testing"), test))] -pub(in crate::transaction::transaction_v1) fn has_valid_activate_bid_args( - args: &RuntimeArgs, -) -> Result<(), InvalidTransactionV1> { +pub fn has_valid_activate_bid_args(args: &RuntimeArgs) -> Result<(), InvalidTransactionV1> { let _validator = ACTIVATE_BID_ARG_VALIDATOR.get(args)?; Ok(()) } /// Checks the given `RuntimeArgs` are suitable for use in a change bid public key transaction. #[allow(dead_code)] -pub(super) fn has_valid_change_bid_public_key_args( +#[cfg(any(all(feature = "std", feature = "testing"), test))] +pub fn has_valid_change_bid_public_key_args( args: &RuntimeArgs, ) -> Result<(), InvalidTransactionV1> { let _public_key = CHANGE_BID_PUBLIC_KEY_ARG_PUBLIC_KEY.get(args)?; diff --git a/types/src/transaction/transaction_v1/errors_v1.rs b/types/src/transaction/transaction_v1/errors_v1.rs index bc0ef906e2..b0ae614f70 100644 --- a/types/src/transaction/transaction_v1/errors_v1.rs +++ b/types/src/transaction/transaction_v1/errors_v1.rs @@ -10,10 +10,21 @@ use std::error::Error as StdError; use datasize::DataSize; use serde::Serialize; -use super::super::TransactionEntryPoint; #[cfg(doc)] use super::TransactionV1; -use crate::{bytesrepr, crypto, CLType, DisplayIter, PricingMode, TimeDiff, Timestamp, U512}; +use crate::{ + bytesrepr, crypto, CLType, DisplayIter, PricingMode, TimeDiff, Timestamp, + TransactionEntryPoint, U512, +}; + +#[derive(Clone, Eq, PartialEq, Debug)] +#[cfg_attr(feature = "std", derive(Serialize))] +#[cfg_attr(feature = "datasize", derive(DataSize))] +pub enum FieldDeserializationError { + IndexNotExists { index: u16 }, + FromBytesError { index: u16, error: bytesrepr::Error }, + LingeringBytesInField { index: u16 }, +} /// Returned when a [`TransactionV1`] fails validation. #[derive(Clone, Eq, PartialEq, Debug)] @@ -130,12 +141,16 @@ pub enum InvalidTransaction { /// The invalid entry point. entry_point: TransactionEntryPoint, }, - /// The entry point for this transaction target must be `TransactionEntryPoint::Custom`. EntryPointMustBeCustom { /// The invalid entry point. entry_point: TransactionEntryPoint, }, + /// The entry point for this transaction target must be `TransactionEntryPoint::Call`. + EntryPointMustBeCall { + /// The invalid entry point. + entry_point: TransactionEntryPoint, + }, /// The transaction has empty module bytes. EmptyModuleBytes, /// Attempt to factor the amount over the gas_price failed. @@ -155,7 +170,9 @@ pub enum InvalidTransaction { price_mode: PricingMode, }, /// The transaction provided is not supported. - InvalidTransactionKind(u8), + InvalidTransactionLane(u8), + /// No wasm lane matches transaction + NoWasmLaneMatchesTransaction(), /// Gas price tolerance too low. GasPriceToleranceTooLow { /// The minimum gas price tolerance. @@ -163,6 +180,17 @@ pub enum InvalidTransaction { /// The provided gas price tolerance. provided_gas_price_tolerance: u8, }, + /// Error when trying to deserialize one of the transactionV1 payload fields. + CouldNotDeserializeField { + /// Underlying reason why the deserialization failed + error: FieldDeserializationError, + }, + + /// Unable to calculate hash for payloads transaction. + CannotCalculateFieldsHash, + + /// The transactions field map had entries that were unexpected + UnexpectedTransactionFieldEntries, } impl Display for InvalidTransaction { @@ -295,7 +323,7 @@ impl Display for InvalidTransaction { "received a transaction with an invalid mode {price_mode}" ) } - InvalidTransaction::InvalidTransactionKind(kind) => { + InvalidTransaction::InvalidTransactionLane(kind) => { write!( formatter, "received a transaction with an invalid kind {kind}" @@ -311,6 +339,35 @@ impl Display for InvalidTransaction { provided_gas_price_tolerance, min_gas_price_tolerance ) } + InvalidTransaction::CouldNotDeserializeField { error } => { + match error { + FieldDeserializationError::IndexNotExists { index } => write!( + formatter, + "tried to deserialize a field under index {} but it is not present in the payload", + index + ), + FieldDeserializationError::FromBytesError { index, error } => write!( + formatter, + "tried to deserialize a field under index {} but it failed with error: {}", + index, + error + ), + FieldDeserializationError::LingeringBytesInField { index } => write!( + formatter, + "tried to deserialize a field under index {} but after deserialization there were still bytes left", + index, + ), + } + }, + InvalidTransaction::CannotCalculateFieldsHash => write!( + formatter, + "cannot calculate a hash digest for the transaction" + ), + InvalidTransaction::EntryPointMustBeCall { entry_point } => { + write!(formatter, "entry point must be call: {entry_point}") + }, + InvalidTransaction::NoWasmLaneMatchesTransaction() => write!(formatter, "Could not match any generic wasm lane to the specified transaction"), + InvalidTransaction::UnexpectedTransactionFieldEntries => write!(formatter, "There were entries in the fields map of the payload that could not be matched"), } } } @@ -343,13 +400,22 @@ impl StdError for InvalidTransaction { | InvalidTransaction::EntryPointCannotBeCall | InvalidTransaction::EntryPointCannotBeCustom { .. } | InvalidTransaction::EntryPointMustBeCustom { .. } + | InvalidTransaction::EntryPointMustBeCall { .. } | InvalidTransaction::EmptyModuleBytes | InvalidTransaction::GasPriceConversion { .. } | InvalidTransaction::UnableToCalculateGasLimit | InvalidTransaction::UnableToCalculateGasCost | InvalidTransaction::InvalidPricingMode { .. } | InvalidTransaction::GasPriceToleranceTooLow { .. } - | InvalidTransaction::InvalidTransactionKind(_) => None, + | InvalidTransaction::InvalidTransactionLane(_) + | InvalidTransaction::CannotCalculateFieldsHash + | InvalidTransaction::NoWasmLaneMatchesTransaction() + | InvalidTransaction::UnexpectedTransactionFieldEntries => None, + InvalidTransaction::CouldNotDeserializeField { error } => match error { + FieldDeserializationError::IndexNotExists { .. } + | FieldDeserializationError::LingeringBytesInField { .. } => None, + FieldDeserializationError::FromBytesError { error, .. } => Some(error), + }, } } } diff --git a/types/src/transaction/transaction_v1/fields_container.rs b/types/src/transaction/transaction_v1/fields_container.rs new file mode 100644 index 0000000000..557f50215b --- /dev/null +++ b/types/src/transaction/transaction_v1/fields_container.rs @@ -0,0 +1,282 @@ +#[cfg(any(feature = "testing", test))] +use crate::testing::TestRng; +#[cfg(any(feature = "std", feature = "testing", test))] +use crate::{ + bytesrepr::{Bytes, ToBytes}, + transaction::transaction_v1::*, + RuntimeArgs, TransactionEntryPoint, TransactionScheduling, TransactionTarget, +}; +#[cfg(any(feature = "testing", test))] +use crate::{ + PublicKey, TransactionInvocationTarget, TransactionRuntime, TransferTarget, AUCTION_LANE_ID, + INSTALL_UPGRADE_LANE_ID, MINT_LANE_ID, +}; +#[cfg(any(feature = "std", feature = "testing", test))] +use alloc::collections::BTreeMap; +#[cfg(any(feature = "testing", test))] +use rand::{Rng, RngCore}; + +#[cfg(any(feature = "std", feature = "testing", feature = "gens", test))] +pub(crate) const ARGS_MAP_KEY: u16 = 0; +#[cfg(any(feature = "std", feature = "testing", feature = "gens", test))] +pub(crate) const TARGET_MAP_KEY: u16 = 1; +#[cfg(any(feature = "std", feature = "testing", feature = "gens", test))] +pub(crate) const ENTRY_POINT_MAP_KEY: u16 = 2; +#[cfg(any(feature = "std", feature = "testing", feature = "gens", test))] +pub(crate) const SCHEDULING_MAP_KEY: u16 = 3; + +#[cfg(any(feature = "std", feature = "testing", feature = "gens", test))] +#[derive(Clone, Eq, PartialEq, Debug)] +pub(crate) enum FieldsContainerError { + CouldNotSerializeField { field_index: u16 }, +} + +#[cfg(any(feature = "std", feature = "testing", feature = "gens", test))] +pub(crate) struct FieldsContainer { + pub(super) args: RuntimeArgs, + pub(super) target: TransactionTarget, + pub(super) entry_point: TransactionEntryPoint, + pub(super) scheduling: TransactionScheduling, +} + +#[cfg(any(feature = "std", feature = "testing", feature = "gens", test))] +impl FieldsContainer { + pub(crate) fn new( + args: RuntimeArgs, + target: TransactionTarget, + entry_point: TransactionEntryPoint, + scheduling: TransactionScheduling, + ) -> Self { + FieldsContainer { + args, + target, + entry_point, + scheduling, + } + } + + pub(crate) fn to_map(&self) -> Result, FieldsContainerError> { + let mut map: BTreeMap = BTreeMap::new(); + map.insert( + ARGS_MAP_KEY, + self.args.to_bytes().map(Into::into).map_err(|_| { + FieldsContainerError::CouldNotSerializeField { + field_index: ARGS_MAP_KEY, + } + })?, + ); + map.insert( + TARGET_MAP_KEY, + self.target.to_bytes().map(Into::into).map_err(|_| { + FieldsContainerError::CouldNotSerializeField { + field_index: TARGET_MAP_KEY, + } + })?, + ); + map.insert( + ENTRY_POINT_MAP_KEY, + self.entry_point.to_bytes().map(Into::into).map_err(|_| { + FieldsContainerError::CouldNotSerializeField { + field_index: ENTRY_POINT_MAP_KEY, + } + })?, + ); + map.insert( + SCHEDULING_MAP_KEY, + self.scheduling.to_bytes().map(Into::into).map_err(|_| { + FieldsContainerError::CouldNotSerializeField { + field_index: SCHEDULING_MAP_KEY, + } + })?, + ); + Ok(map) + } + + /// Returns a random `FieldsContainer`. + #[cfg(any(feature = "testing", test))] + pub(crate) fn random(rng: &mut TestRng) -> Self { + match rng.gen_range(0..8) { + 0 => { + let amount = rng.gen_range(2_500_000_000..=u64::MAX); + let maybe_source = if rng.gen() { Some(rng.gen()) } else { None }; + let target = TransferTarget::random(rng); + let maybe_id = rng.gen::().then(|| rng.gen()); + let args = arg_handling::new_transfer_args(amount, maybe_source, target, maybe_id) + .unwrap(); + FieldsContainer::new( + args, + TransactionTarget::Native, + TransactionEntryPoint::Transfer, + TransactionScheduling::random(rng), + ) + } + 1 => { + let public_key = PublicKey::random(rng); + let delegation_rate = rng.gen(); + let amount = rng.gen::(); + let minimum_delegation_amount = rng.gen::() as u64; + let maximum_delegation_amount = minimum_delegation_amount + rng.gen::() as u64; + let args = arg_handling::new_add_bid_args( + public_key, + delegation_rate, + amount, + minimum_delegation_amount, + maximum_delegation_amount, + ) + .unwrap(); + FieldsContainer::new( + args, + TransactionTarget::Native, + TransactionEntryPoint::AddBid, + TransactionScheduling::random(rng), + ) + } + 2 => { + let public_key = PublicKey::random(rng); + let amount = rng.gen::(); + let args = arg_handling::new_withdraw_bid_args(public_key, amount).unwrap(); + FieldsContainer::new( + args, + TransactionTarget::Native, + TransactionEntryPoint::WithdrawBid, + TransactionScheduling::random(rng), + ) + } + 3 => { + let delegator = PublicKey::random(rng); + let validator = PublicKey::random(rng); + let amount = rng.gen::(); + let args = arg_handling::new_delegate_args(delegator, validator, amount).unwrap(); + FieldsContainer::new( + args, + TransactionTarget::Native, + TransactionEntryPoint::Delegate, + TransactionScheduling::random(rng), + ) + } + 4 => { + let delegator = PublicKey::random(rng); + let validator = PublicKey::random(rng); + let amount = rng.gen::(); + let args = arg_handling::new_undelegate_args(delegator, validator, amount).unwrap(); + FieldsContainer::new( + args, + TransactionTarget::Native, + TransactionEntryPoint::Undelegate, + TransactionScheduling::random(rng), + ) + } + 5 => { + let delegator = PublicKey::random(rng); + let validator = PublicKey::random(rng); + let amount = rng.gen::(); + let new_validator = PublicKey::random(rng); + let args = + arg_handling::new_redelegate_args(delegator, validator, amount, new_validator) + .unwrap(); + FieldsContainer::new( + args, + TransactionTarget::Native, + TransactionEntryPoint::Redelegate, + TransactionScheduling::random(rng), + ) + } + 6 => Self::random_standard(rng), + 7 => { + let mut buffer = vec![0u8; rng.gen_range(1..100)]; + rng.fill_bytes(buffer.as_mut()); + let is_install_upgrade = rng.gen(); + let target = TransactionTarget::Session { + is_install_upgrade, + module_bytes: Bytes::from(buffer), + runtime: TransactionRuntime::VmCasperV1, + }; + FieldsContainer::new( + RuntimeArgs::random(rng), + target, + TransactionEntryPoint::Call, + TransactionScheduling::random(rng), + ) + } + _ => unreachable!(), + } + } + + /// Returns a random `FieldsContainer`. + #[cfg(any(feature = "testing", test))] + pub fn random_of_lane(rng: &mut TestRng, lane_id: u8) -> Self { + match lane_id { + MINT_LANE_ID => Self::random_transfer(rng), + AUCTION_LANE_ID => Self::random_staking(rng), + INSTALL_UPGRADE_LANE_ID => Self::random_install_upgrade(rng), + _ => Self::random_standard(rng), + } + } + + #[cfg(any(feature = "testing", test))] + fn random_transfer(rng: &mut TestRng) -> Self { + let amount = rng.gen_range(2_500_000_000..=u64::MAX); + let maybe_source = if rng.gen() { Some(rng.gen()) } else { None }; + let target = TransferTarget::random(rng); + let maybe_id = rng.gen::().then(|| rng.gen()); + let args = arg_handling::new_transfer_args(amount, maybe_source, target, maybe_id).unwrap(); + FieldsContainer::new( + args, + TransactionTarget::Native, + TransactionEntryPoint::Transfer, + TransactionScheduling::random(rng), + ) + } + + #[cfg(any(feature = "testing", test))] + fn random_install_upgrade(rng: &mut TestRng) -> Self { + let target = TransactionTarget::Session { + module_bytes: Bytes::from(rng.random_vec(0..100)), + runtime: TransactionRuntime::VmCasperV1, + is_install_upgrade: true, + }; + FieldsContainer::new( + RuntimeArgs::random(rng), + target, + TransactionEntryPoint::Call, + TransactionScheduling::random(rng), + ) + } + + #[cfg(any(feature = "testing", test))] + fn random_staking(rng: &mut TestRng) -> Self { + let public_key = PublicKey::random(rng); + let delegation_rate = rng.gen(); + let amount = rng.gen::(); + let minimum_delegation_amount = rng.gen::() as u64; + let maximum_delegation_amount = minimum_delegation_amount + rng.gen::() as u64; + let args = arg_handling::new_add_bid_args( + public_key, + delegation_rate, + amount, + minimum_delegation_amount, + maximum_delegation_amount, + ) + .unwrap(); + FieldsContainer::new( + args, + TransactionTarget::Native, + TransactionEntryPoint::AddBid, + TransactionScheduling::random(rng), + ) + } + + #[cfg(any(feature = "testing", test))] + fn random_standard(rng: &mut TestRng) -> Self { + let target = TransactionTarget::Stored { + id: TransactionInvocationTarget::random(rng), + runtime: TransactionRuntime::VmCasperV1, + }; + FieldsContainer::new( + RuntimeArgs::random(rng), + target, + TransactionEntryPoint::Custom(rng.random_string(1..11)), + TransactionScheduling::random(rng), + ) + } +} diff --git a/types/src/transaction/transaction_v1/transaction_v1_body.rs b/types/src/transaction/transaction_v1/transaction_v1_body.rs deleted file mode 100644 index fec2259311..0000000000 --- a/types/src/transaction/transaction_v1/transaction_v1_body.rs +++ /dev/null @@ -1,661 +0,0 @@ -#[cfg(any(feature = "std", test))] -pub(super) mod arg_handling; - -use alloc::vec::Vec; -use core::fmt::{self, Display, Formatter}; - -use super::super::{RuntimeArgs, TransactionEntryPoint, TransactionScheduling, TransactionTarget}; -#[cfg(feature = "datasize")] -use datasize::DataSize; -#[cfg(any(all(feature = "std", feature = "testing"), test))] -use rand::{Rng, RngCore}; -#[cfg(feature = "json-schema")] -use schemars::JsonSchema; -#[cfg(any(feature = "std", test))] -use serde::{Deserialize, Serialize}; -#[cfg(any(all(feature = "std", feature = "testing"), test))] -use tracing::debug; - -use super::TransactionCategory; -#[cfg(any(all(feature = "std", feature = "testing"), test))] -use super::TransactionConfig; -#[cfg(doc)] -use super::TransactionV1; -#[cfg(any(feature = "std", test))] -use crate::InvalidTransactionV1; -#[cfg(any(all(feature = "std", feature = "testing"), test))] -use crate::TransactionV1ExcessiveSizeError; -#[cfg(any(all(feature = "std", feature = "testing"), test))] -use crate::{ - bytesrepr::Bytes, testing::TestRng, PublicKey, TransactionInvocationTarget, TransactionRuntime, - TransferTarget, -}; -use crate::{ - bytesrepr::{Error, FromBytes, ToBytes}, - transaction::serialization::{ - CalltableSerializationEnvelope, CalltableSerializationEnvelopeBuilder, - }, -}; -/// The body of a [`TransactionV1`]. -#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)] -#[cfg_attr( - any(feature = "std", test), - derive(Serialize, Deserialize), - serde(deny_unknown_fields) -)] -#[cfg_attr(feature = "datasize", derive(DataSize))] -#[cfg_attr( - feature = "json-schema", - derive(JsonSchema), - schemars(description = "Body of a `TransactionV1`.") -)] -pub struct TransactionV1Body { - pub(crate) args: RuntimeArgs, - pub(crate) target: TransactionTarget, - pub(crate) entry_point: TransactionEntryPoint, - pub(crate) transaction_category: u8, - pub(crate) scheduling: TransactionScheduling, -} - -impl TransactionV1Body { - /// Returns a new `TransactionV1Body`. - pub fn new( - args: RuntimeArgs, - target: TransactionTarget, - entry_point: TransactionEntryPoint, - transaction_category: u8, - scheduling: TransactionScheduling, - ) -> Self { - TransactionV1Body { - args, - target, - entry_point, - transaction_category, - scheduling, - } - } - - /// Returns the runtime args of the transaction. - pub fn args(&self) -> &RuntimeArgs { - &self.args - } - - /// Consumes `self`, returning the runtime args of the transaction. - pub fn take_args(self) -> RuntimeArgs { - self.args - } - - /// Returns the target of the transaction. - pub fn target(&self) -> &TransactionTarget { - &self.target - } - - /// Returns the entry point of the transaction. - pub fn entry_point(&self) -> &TransactionEntryPoint { - &self.entry_point - } - - /// Returns the scheduling kind of the transaction. - pub fn scheduling(&self) -> &TransactionScheduling { - &self.scheduling - } - - /// Returns true if this transaction is a native mint interaction. - pub fn is_native_mint(&self) -> bool { - self.transaction_category == TransactionCategory::Mint as u8 - } - - /// Returns true if this transaction is a native auction interaction. - pub fn is_native_auction(&self) -> bool { - self.transaction_category == TransactionCategory::Auction as u8 - } - - /// Returns true if this transaction is a smart contract installer or upgrader. - pub fn is_install_or_upgrade(&self) -> bool { - self.transaction_category == TransactionCategory::InstallUpgrade as u8 - } - - /// Returns the transaction category. - pub fn transaction_category(&self) -> u8 { - self.transaction_category - } - - /// Consumes `self`, returning its constituent parts. - pub fn destructure( - self, - ) -> ( - RuntimeArgs, - TransactionTarget, - TransactionEntryPoint, - TransactionScheduling, - ) { - (self.args, self.target, self.entry_point, self.scheduling) - } - - #[cfg(any(all(feature = "std", feature = "testing"), test))] - pub(super) fn is_valid(&self, config: &TransactionConfig) -> Result<(), InvalidTransactionV1> { - let kind = self.transaction_category; - if !config.transaction_v1_config.is_supported(kind) { - return Err(InvalidTransactionV1::InvalidTransactionKind( - self.transaction_category, - )); - } - - let max_serialized_length = config.transaction_v1_config.get_max_serialized_length(kind); - let actual_length = self.serialized_length(); - if actual_length > max_serialized_length as usize { - return Err(InvalidTransactionV1::ExcessiveSize( - TransactionV1ExcessiveSizeError { - max_transaction_size: max_serialized_length as u32, - actual_transaction_size: actual_length, - }, - )); - } - - let max_args_length = config.transaction_v1_config.get_max_args_length(kind); - - let args_length = self.args.serialized_length(); - if args_length > max_args_length as usize { - debug!( - args_length, - max_args_length = max_args_length, - "transaction runtime args excessive size" - ); - return Err(InvalidTransactionV1::ExcessiveArgsLength { - max_length: max_args_length as usize, - got: args_length, - }); - } - - match &self.target { - TransactionTarget::Native => match self.entry_point { - TransactionEntryPoint::Call => { - debug!( - entry_point = %self.entry_point, - "native transaction cannot have call entry point" - ); - Err(InvalidTransactionV1::EntryPointCannotBeCall) - } - TransactionEntryPoint::Custom(_) => { - debug!( - entry_point = %self.entry_point, - "native transaction cannot have custom entry point" - ); - Err(InvalidTransactionV1::EntryPointCannotBeCustom { - entry_point: self.entry_point.clone(), - }) - } - TransactionEntryPoint::Transfer => arg_handling::has_valid_transfer_args( - &self.args, - config.native_transfer_minimum_motes, - ), - TransactionEntryPoint::AddBid => arg_handling::has_valid_add_bid_args(&self.args), - TransactionEntryPoint::WithdrawBid => { - arg_handling::has_valid_withdraw_bid_args(&self.args) - } - TransactionEntryPoint::Delegate => { - arg_handling::has_valid_delegate_args(&self.args) - } - TransactionEntryPoint::Undelegate => { - arg_handling::has_valid_undelegate_args(&self.args) - } - TransactionEntryPoint::Redelegate => { - arg_handling::has_valid_redelegate_args(&self.args) - } - TransactionEntryPoint::ActivateBid => { - arg_handling::has_valid_activate_bid_args(&self.args) - } - TransactionEntryPoint::ChangeBidPublicKey => { - arg_handling::has_valid_change_bid_public_key_args(&self.args) - } - TransactionEntryPoint::AddReservations => { - todo!() - } - TransactionEntryPoint::CancelReservations => { - todo!() - } - }, - TransactionTarget::Stored { .. } => match &self.entry_point { - TransactionEntryPoint::Custom(_) => Ok(()), - TransactionEntryPoint::Call - | TransactionEntryPoint::Transfer - | TransactionEntryPoint::AddBid - | TransactionEntryPoint::WithdrawBid - | TransactionEntryPoint::Delegate - | TransactionEntryPoint::Undelegate - | TransactionEntryPoint::Redelegate - | TransactionEntryPoint::ActivateBid - | TransactionEntryPoint::ChangeBidPublicKey - | TransactionEntryPoint::AddReservations - | TransactionEntryPoint::CancelReservations => { - debug!( - entry_point = %self.entry_point, - "transaction targeting stored entity/package must have custom entry point" - ); - Err(InvalidTransactionV1::EntryPointMustBeCustom { - entry_point: self.entry_point.clone(), - }) - } - }, - TransactionTarget::Session { module_bytes, .. } => match &self.entry_point { - TransactionEntryPoint::Call | TransactionEntryPoint::Custom(_) => { - if module_bytes.is_empty() { - debug!("transaction with session code must not have empty module bytes"); - return Err(InvalidTransactionV1::EmptyModuleBytes); - } - Ok(()) - } - TransactionEntryPoint::Transfer - | TransactionEntryPoint::AddBid - | TransactionEntryPoint::WithdrawBid - | TransactionEntryPoint::Delegate - | TransactionEntryPoint::Undelegate - | TransactionEntryPoint::Redelegate - | TransactionEntryPoint::ActivateBid - | TransactionEntryPoint::ChangeBidPublicKey - | TransactionEntryPoint::AddReservations - | TransactionEntryPoint::CancelReservations => { - debug!( - entry_point = %self.entry_point, - "transaction with session code must use custom or default 'call' entry point" - ); - Err(InvalidTransactionV1::EntryPointMustBeCustom { - entry_point: self.entry_point.clone(), - }) - } - }, - } - } - - fn serialized_field_lengths(&self) -> Vec { - vec![ - self.args.serialized_length(), - self.target.serialized_length(), - self.entry_point.serialized_length(), - self.transaction_category.serialized_length(), - self.scheduling.serialized_length(), - ] - } - - /// Returns a random `TransactionV1Body`. - #[cfg(any(all(feature = "std", feature = "testing"), test))] - pub fn random_of_category(rng: &mut TestRng, category: u8) -> Self { - match category { - 0 => Self::random_transfer(rng), - 1 => Self::random_staking(rng), - 2 => Self::random_install_upgrade(rng), - _ => Self::random_standard(rng), - } - } - - #[cfg(any(all(feature = "std", feature = "testing"), test))] - fn random_transfer(rng: &mut TestRng) -> Self { - let amount = - rng.gen_range(TransactionConfig::default().native_transfer_minimum_motes..=u64::MAX); - let maybe_source = if rng.gen() { Some(rng.gen()) } else { None }; - let target = TransferTarget::random(rng); - let maybe_id = rng.gen::().then(|| rng.gen()); - let args = arg_handling::new_transfer_args(amount, maybe_source, target, maybe_id).unwrap(); - TransactionV1Body::new( - args, - TransactionTarget::Native, - TransactionEntryPoint::Transfer, - TransactionCategory::Mint as u8, - TransactionScheduling::random(rng), - ) - } - - #[cfg(any(all(feature = "std", feature = "testing"), test))] - fn random_standard(rng: &mut TestRng) -> Self { - let target = TransactionTarget::Stored { - id: TransactionInvocationTarget::random(rng), - runtime: TransactionRuntime::VmCasperV1, - }; - TransactionV1Body::new( - RuntimeArgs::random(rng), - target, - TransactionEntryPoint::Custom(rng.random_string(1..11)), - TransactionCategory::Large as u8, - TransactionScheduling::random(rng), - ) - } - - #[cfg(any(all(feature = "std", feature = "testing"), test))] - fn random_install_upgrade(rng: &mut TestRng) -> Self { - let target = TransactionTarget::Session { - module_bytes: Bytes::from(rng.random_vec(0..100)), - runtime: TransactionRuntime::VmCasperV1, - }; - TransactionV1Body::new( - RuntimeArgs::random(rng), - target, - TransactionEntryPoint::Custom(rng.random_string(1..11)), - TransactionCategory::InstallUpgrade as u8, - TransactionScheduling::random(rng), - ) - } - - #[cfg(any(all(feature = "std", feature = "testing"), test))] - fn random_staking(rng: &mut TestRng) -> Self { - let public_key = PublicKey::random(rng); - let delegation_rate = rng.gen(); - let amount = rng.gen::(); - let minimum_delegation_amount = rng.gen::() as u64; - let maximum_delegation_amount = minimum_delegation_amount + rng.gen::() as u64; - let args = arg_handling::new_add_bid_args( - public_key, - delegation_rate, - amount, - minimum_delegation_amount, - maximum_delegation_amount, - ) - .unwrap(); - TransactionV1Body::new( - args, - TransactionTarget::Native, - TransactionEntryPoint::AddBid, - TransactionCategory::Auction as u8, - TransactionScheduling::random(rng), - ) - } - - /// Returns a random `TransactionV1Body`. - #[cfg(any(all(feature = "std", feature = "testing"), test))] - pub fn random(rng: &mut TestRng) -> Self { - match rng.gen_range(0..8) { - 0 => { - let amount = rng.gen_range( - TransactionConfig::default().native_transfer_minimum_motes..=u64::MAX, - ); - let maybe_source = if rng.gen() { Some(rng.gen()) } else { None }; - let target = TransferTarget::random(rng); - let maybe_id = rng.gen::().then(|| rng.gen()); - let args = arg_handling::new_transfer_args(amount, maybe_source, target, maybe_id) - .unwrap(); - TransactionV1Body::new( - args, - TransactionTarget::Native, - TransactionEntryPoint::Transfer, - TransactionCategory::Mint as u8, - TransactionScheduling::random(rng), - ) - } - 1 => { - let public_key = PublicKey::random(rng); - let delegation_rate = rng.gen(); - let amount = rng.gen::(); - let minimum_delegation_amount = rng.gen::() as u64; - let maximum_delegation_amount = minimum_delegation_amount + rng.gen::() as u64; - let args = arg_handling::new_add_bid_args( - public_key, - delegation_rate, - amount, - minimum_delegation_amount, - maximum_delegation_amount, - ) - .unwrap(); - TransactionV1Body::new( - args, - TransactionTarget::Native, - TransactionEntryPoint::AddBid, - TransactionCategory::Auction as u8, - TransactionScheduling::random(rng), - ) - } - 2 => { - let public_key = PublicKey::random(rng); - let amount = rng.gen::(); - let args = arg_handling::new_withdraw_bid_args(public_key, amount).unwrap(); - TransactionV1Body::new( - args, - TransactionTarget::Native, - TransactionEntryPoint::WithdrawBid, - TransactionCategory::Auction as u8, - TransactionScheduling::random(rng), - ) - } - 3 => { - let delegator = PublicKey::random(rng); - let validator = PublicKey::random(rng); - let amount = rng.gen::(); - let args = arg_handling::new_delegate_args(delegator, validator, amount).unwrap(); - TransactionV1Body::new( - args, - TransactionTarget::Native, - TransactionEntryPoint::Delegate, - TransactionCategory::Auction as u8, - TransactionScheduling::random(rng), - ) - } - 4 => { - let delegator = PublicKey::random(rng); - let validator = PublicKey::random(rng); - let amount = rng.gen::(); - let args = arg_handling::new_undelegate_args(delegator, validator, amount).unwrap(); - TransactionV1Body::new( - args, - TransactionTarget::Native, - TransactionEntryPoint::Undelegate, - TransactionCategory::Auction as u8, - TransactionScheduling::random(rng), - ) - } - 5 => { - let delegator = PublicKey::random(rng); - let validator = PublicKey::random(rng); - let amount = rng.gen::(); - let new_validator = PublicKey::random(rng); - let args = - arg_handling::new_redelegate_args(delegator, validator, amount, new_validator) - .unwrap(); - TransactionV1Body::new( - args, - TransactionTarget::Native, - TransactionEntryPoint::Redelegate, - TransactionCategory::Auction as u8, - TransactionScheduling::random(rng), - ) - } - 6 => Self::random_standard(rng), - 7 => { - let mut buffer = vec![0u8; rng.gen_range(1..100)]; - rng.fill_bytes(buffer.as_mut()); - let target = TransactionTarget::Session { - module_bytes: Bytes::from(buffer), - runtime: TransactionRuntime::VmCasperV1, - }; - TransactionV1Body::new( - RuntimeArgs::random(rng), - target, - TransactionEntryPoint::Custom(rng.random_string(1..11)), - TransactionCategory::Large as u8, - TransactionScheduling::random(rng), - ) - } - _ => unreachable!(), - } - } -} - -const ARGS_INDEX: u16 = 0; -const TARGET_INDEX: u16 = 1; -const ENTRY_POINT_INDEX: u16 = 2; -const TRANSACTION_CATEGORY_INDEX: u16 = 3; -const SCHEDULING_INDEX: u16 = 4; - -impl FromBytes for TransactionV1Body { - fn from_bytes(bytes: &[u8]) -> Result<(TransactionV1Body, &[u8]), Error> { - let (binary_payload, remainder) = - crate::transaction::serialization::CalltableSerializationEnvelope::from_bytes( - 5, bytes, - )?; - let window = binary_payload.start_consuming()?; - let window = window.ok_or(Error::Formatting)?; - window.verify_index(ARGS_INDEX)?; - let (args, window) = window.deserialize_and_maybe_next::()?; - let window = window.ok_or(Error::Formatting)?; - window.verify_index(TARGET_INDEX)?; - let (target, window) = window.deserialize_and_maybe_next::()?; - let window = window.ok_or(Error::Formatting)?; - window.verify_index(ENTRY_POINT_INDEX)?; - let (entry_point, window) = window.deserialize_and_maybe_next::()?; - let window = window.ok_or(Error::Formatting)?; - window.verify_index(TRANSACTION_CATEGORY_INDEX)?; - let (transaction_category, window) = window.deserialize_and_maybe_next::()?; - let window = window.ok_or(Error::Formatting)?; - window.verify_index(SCHEDULING_INDEX)?; - let (scheduling, window) = window.deserialize_and_maybe_next::()?; - if window.is_some() { - return Err(Error::Formatting); - } - let from_bytes = TransactionV1Body { - args, - target, - entry_point, - transaction_category, - scheduling, - }; - Ok((from_bytes, remainder)) - } -} - -impl ToBytes for TransactionV1Body { - fn to_bytes(&self) -> Result, Error> { - CalltableSerializationEnvelopeBuilder::new(self.serialized_field_lengths())? - .add_field(ARGS_INDEX, &self.args)? - .add_field(TARGET_INDEX, &self.target)? - .add_field(ENTRY_POINT_INDEX, &self.entry_point)? - .add_field(TRANSACTION_CATEGORY_INDEX, &self.transaction_category)? - .add_field(SCHEDULING_INDEX, &self.scheduling)? - .binary_payload_bytes() - } - fn serialized_length(&self) -> usize { - CalltableSerializationEnvelope::estimate_size(self.serialized_field_lengths()) - } -} - -impl Display for TransactionV1Body { - fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { - write!( - formatter, - "v1-body({} {} {})", - self.target, self.entry_point, self.scheduling - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{bytesrepr, runtime_args}; - - #[test] - fn bytesrepr_roundtrip() { - let rng = &mut TestRng::new(); - let body = TransactionV1Body::random(rng); - bytesrepr::test_serialization_roundtrip(&body); - } - - #[test] - fn not_acceptable_due_to_excessive_args_length() { - let rng = &mut TestRng::new(); - let mut config = TransactionConfig::default(); - let mut body = TransactionV1Body::random_standard(rng); - config.transaction_v1_config.wasm_lanes = - vec![vec![body.transaction_category as u64, 1_048_576, 10, 0]]; - body.args = runtime_args! {"a" => 1_u8}; - - let expected_error = InvalidTransactionV1::ExcessiveArgsLength { - max_length: 10, - got: 15, - }; - - assert_eq!(body.is_valid(&config), Err(expected_error)); - } - - #[test] - fn not_acceptable_due_to_custom_entry_point_in_native() { - let rng = &mut TestRng::new(); - let public_key = PublicKey::random(rng); - let amount = rng.gen::(); - let args = arg_handling::new_withdraw_bid_args(public_key, amount).unwrap(); - let entry_point = TransactionEntryPoint::Custom("custom".to_string()); - let body = TransactionV1Body::new( - args, - TransactionTarget::Native, - entry_point.clone(), - TransactionCategory::Mint as u8, - TransactionScheduling::random(rng), - ); - - let expected_error = InvalidTransactionV1::EntryPointCannotBeCustom { entry_point }; - - let config = TransactionConfig::default(); - assert_eq!(body.is_valid(&config), Err(expected_error)); - } - - #[test] - fn not_acceptable_due_to_call_entry_point_in_native() { - let rng = &mut TestRng::new(); - let public_key = PublicKey::random(rng); - let amount = rng.gen::(); - let args = arg_handling::new_withdraw_bid_args(public_key, amount).unwrap(); - let entry_point = TransactionEntryPoint::Call; - let body = TransactionV1Body::new( - args, - TransactionTarget::Native, - entry_point, - TransactionCategory::Mint as u8, - TransactionScheduling::random(rng), - ); - - let expected_error = InvalidTransactionV1::EntryPointCannotBeCall; - - let config = TransactionConfig::default(); - assert_eq!(body.is_valid(&config,), Err(expected_error)); - } - - #[test] - fn not_acceptable_due_to_non_custom_entry_point_in_stored_or_session() { - let rng = &mut TestRng::new(); - let config = TransactionConfig::default(); - - let mut check = |entry_point: TransactionEntryPoint| { - let stored_target = TransactionTarget::new_stored( - TransactionInvocationTarget::ByHash([0; 32]), - TransactionRuntime::VmCasperV1, - ); - let session_target = TransactionTarget::new_session( - Bytes::from(vec![1]), - TransactionRuntime::VmCasperV1, - ); - - let stored_body = TransactionV1Body::new( - RuntimeArgs::new(), - stored_target, - entry_point.clone(), - TransactionCategory::Large as u8, - TransactionScheduling::random(rng), - ); - let session_body = TransactionV1Body::new( - RuntimeArgs::new(), - session_target, - entry_point.clone(), - TransactionCategory::Large as u8, - TransactionScheduling::random(rng), - ); - - let expected_error = InvalidTransactionV1::EntryPointMustBeCustom { entry_point }; - - assert_eq!(stored_body.is_valid(&config), Err(expected_error.clone())); - assert_eq!(session_body.is_valid(&config), Err(expected_error)); - }; - - check(TransactionEntryPoint::Transfer); - check(TransactionEntryPoint::AddBid); - check(TransactionEntryPoint::WithdrawBid); - check(TransactionEntryPoint::Delegate); - check(TransactionEntryPoint::Undelegate); - check(TransactionEntryPoint::Redelegate); - } -} diff --git a/types/src/transaction/transaction_v1/transaction_v1_builder.rs b/types/src/transaction/transaction_v1/transaction_v1_builder.rs index aa36231934..bac581c741 100644 --- a/types/src/transaction/transaction_v1/transaction_v1_builder.rs +++ b/types/src/transaction/transaction_v1/transaction_v1_builder.rs @@ -1,26 +1,25 @@ -mod error; +pub mod error; use core::marker::PhantomData; -#[cfg(any(feature = "testing", test))] -use rand::Rng; - use super::{ - super::{ - InitiatorAddr, TransactionEntryPoint, TransactionInvocationTarget, TransactionRuntime, - TransactionScheduling, TransactionTarget, - }, - transaction_v1_body::arg_handling, - InitiatorAddrAndSecretKey, PricingMode, TransactionV1, TransactionV1Body, + super::{InitiatorAddr, TransactionRuntime, TransactionScheduling, TransactionTarget}, + arg_handling, + fields_container::FieldsContainerError, + InitiatorAddrAndSecretKey, PricingMode, TransactionV1, }; use crate::{ - bytesrepr::Bytes, transaction::TransactionCategory, AddressableEntityHash, CLValue, - CLValueError, EntityVersion, PackageHash, PublicKey, RuntimeArgs, SecretKey, TimeDiff, - Timestamp, TransferTarget, URef, U512, + bytesrepr::Bytes, transaction::FieldsContainer, AddressableEntityHash, CLValue, CLValueError, + EntityVersion, PackageHash, PublicKey, RuntimeArgs, SecretKey, TimeDiff, Timestamp, + TransactionEntryPoint, TransactionInvocationTarget, TransferTarget, URef, U512, }; #[cfg(any(feature = "testing", test))] use crate::{testing::TestRng, transaction::Approval, TransactionConfig, TransactionV1Hash}; +#[cfg(any(all(feature = "std", feature = "testing"), test))] +use alloc::collections::BTreeMap; pub use error::TransactionV1BuilderError; +#[cfg(any(all(feature = "std", feature = "testing"), test))] +use rand::Rng; /// A builder for constructing a [`TransactionV1`]. /// @@ -36,18 +35,23 @@ pub use error::TransactionV1BuilderError; /// It can be signed later (multiple times if desired) to make it valid before sending to the /// network for execution. pub struct TransactionV1Builder<'a> { + args: RuntimeArgs, + target: TransactionTarget, + scheduling: TransactionScheduling, + entry_point: TransactionEntryPoint, chain_name: Option, timestamp: Timestamp, ttl: TimeDiff, - body: TransactionV1Body, pricing_mode: PricingMode, initiator_addr: Option, #[cfg(not(any(feature = "testing", test)))] secret_key: Option<&'a SecretKey>, - #[cfg(any(feature = "testing", test))] + #[cfg(any(all(feature = "std", feature = "testing"), test))] secret_key: Option, - #[cfg(any(feature = "testing", test))] + #[cfg(any(all(feature = "std", feature = "testing"), test))] invalid_approvals: Vec, + #[cfg(any(all(feature = "std", feature = "testing"), test))] + additional_fields: BTreeMap, _phantom_data: PhantomData<&'a ()>, } @@ -57,24 +61,30 @@ impl<'a> TransactionV1Builder<'a> { /// The default pricing mode for v1 transactions, ie FIXED cost. pub const DEFAULT_PRICING_MODE: PricingMode = PricingMode::Fixed { gas_price_tolerance: 5, + additional_computation_factor: 0, }; /// The default runtime for transactions, i.e. Casper Version 1 Virtual Machine. pub const DEFAULT_RUNTIME: TransactionRuntime = TransactionRuntime::VmCasperV1; /// The default scheduling for transactions, i.e. `Standard`. pub const DEFAULT_SCHEDULING: TransactionScheduling = TransactionScheduling::Standard; - pub(super) fn new(body: TransactionV1Body) -> Self { + pub(super) fn new() -> Self { TransactionV1Builder { + args: RuntimeArgs::new(), + entry_point: TransactionEntryPoint::Transfer, + target: TransactionTarget::Native, + scheduling: TransactionScheduling::Standard, chain_name: None, timestamp: Timestamp::now(), ttl: Self::DEFAULT_TTL, - body, pricing_mode: Self::DEFAULT_PRICING_MODE, initiator_addr: None, secret_key: None, _phantom_data: PhantomData, - #[cfg(any(feature = "testing", test))] + #[cfg(any(all(feature = "std", feature = "testing"), test))] invalid_approvals: vec![], + #[cfg(any(all(feature = "std", feature = "testing"), test))] + additional_fields: BTreeMap::new(), } } @@ -86,14 +96,12 @@ impl<'a> TransactionV1Builder<'a> { maybe_id: Option, ) -> Result { let args = arg_handling::new_transfer_args(amount, maybe_source, target, maybe_id)?; - let body = TransactionV1Body::new( - args, - TransactionTarget::Native, - TransactionEntryPoint::Transfer, - TransactionCategory::Mint as u8, - Self::DEFAULT_SCHEDULING, - ); - Ok(TransactionV1Builder::new(body)) + let mut builder = TransactionV1Builder::new(); + builder.args = args; + builder.target = TransactionTarget::Native; + builder.entry_point = TransactionEntryPoint::Transfer; + builder.scheduling = Self::DEFAULT_SCHEDULING; + Ok(builder) } /// Returns a new `TransactionV1Builder` suitable for building a native add_bid transaction. @@ -111,14 +119,12 @@ impl<'a> TransactionV1Builder<'a> { minimum_delegation_amount, maximum_delegation_amount, )?; - let body = TransactionV1Body::new( - args, - TransactionTarget::Native, - TransactionEntryPoint::AddBid, - TransactionCategory::Auction as u8, - Self::DEFAULT_SCHEDULING, - ); - Ok(TransactionV1Builder::new(body)) + let mut builder = TransactionV1Builder::new(); + builder.args = args; + builder.target = TransactionTarget::Native; + builder.entry_point = TransactionEntryPoint::AddBid; + builder.scheduling = Self::DEFAULT_SCHEDULING; + Ok(builder) } /// Returns a new `TransactionV1Builder` suitable for building a native withdraw_bid @@ -128,14 +134,12 @@ impl<'a> TransactionV1Builder<'a> { amount: A, ) -> Result { let args = arg_handling::new_withdraw_bid_args(public_key, amount)?; - let body = TransactionV1Body::new( - args, - TransactionTarget::Native, - TransactionEntryPoint::WithdrawBid, - TransactionCategory::Auction as u8, - Self::DEFAULT_SCHEDULING, - ); - Ok(TransactionV1Builder::new(body)) + let mut builder = TransactionV1Builder::new(); + builder.args = args; + builder.target = TransactionTarget::Native; + builder.entry_point = TransactionEntryPoint::WithdrawBid; + builder.scheduling = Self::DEFAULT_SCHEDULING; + Ok(builder) } /// Returns a new `TransactionV1Builder` suitable for building a native delegate transaction. @@ -145,14 +149,12 @@ impl<'a> TransactionV1Builder<'a> { amount: A, ) -> Result { let args = arg_handling::new_delegate_args(delegator, validator, amount)?; - let body = TransactionV1Body::new( - args, - TransactionTarget::Native, - TransactionEntryPoint::Delegate, - TransactionCategory::Auction as u8, - Self::DEFAULT_SCHEDULING, - ); - Ok(TransactionV1Builder::new(body)) + let mut builder = TransactionV1Builder::new(); + builder.args = args; + builder.target = TransactionTarget::Native; + builder.entry_point = TransactionEntryPoint::Delegate; + builder.scheduling = Self::DEFAULT_SCHEDULING; + Ok(builder) } /// Returns a new `TransactionV1Builder` suitable for building a native undelegate transaction. @@ -162,14 +164,12 @@ impl<'a> TransactionV1Builder<'a> { amount: A, ) -> Result { let args = arg_handling::new_undelegate_args(delegator, validator, amount)?; - let body = TransactionV1Body::new( - args, - TransactionTarget::Native, - TransactionEntryPoint::Undelegate, - TransactionCategory::Auction as u8, - Self::DEFAULT_SCHEDULING, - ); - Ok(TransactionV1Builder::new(body)) + let mut builder = TransactionV1Builder::new(); + builder.args = args; + builder.target = TransactionTarget::Native; + builder.entry_point = TransactionEntryPoint::Undelegate; + builder.scheduling = Self::DEFAULT_SCHEDULING; + Ok(builder) } /// Returns a new `TransactionV1Builder` suitable for building a native redelegate transaction. @@ -180,14 +180,12 @@ impl<'a> TransactionV1Builder<'a> { new_validator: PublicKey, ) -> Result { let args = arg_handling::new_redelegate_args(delegator, validator, amount, new_validator)?; - let body = TransactionV1Body::new( - args, - TransactionTarget::Native, - TransactionEntryPoint::Redelegate, - TransactionCategory::Auction as u8, - Self::DEFAULT_SCHEDULING, - ); - Ok(TransactionV1Builder::new(body)) + let mut builder = TransactionV1Builder::new(); + builder.args = args; + builder.target = TransactionTarget::Native; + builder.entry_point = TransactionEntryPoint::Redelegate; + builder.scheduling = Self::DEFAULT_SCHEDULING; + Ok(builder) } fn new_targeting_stored>( @@ -198,14 +196,12 @@ impl<'a> TransactionV1Builder<'a> { id, runtime: Self::DEFAULT_RUNTIME, }; - let body = TransactionV1Body::new( - RuntimeArgs::new(), - target, - TransactionEntryPoint::Custom(entry_point.into()), - TransactionCategory::Large as u8, - Self::DEFAULT_SCHEDULING, - ); - TransactionV1Builder::new(body) + let mut builder = TransactionV1Builder::new(); + builder.args = RuntimeArgs::new(); + builder.target = target; + builder.entry_point = TransactionEntryPoint::Custom(entry_point.into()); + builder.scheduling = Self::DEFAULT_SCHEDULING; + builder } /// Returns a new `TransactionV1Builder` suitable for building a transaction targeting a stored @@ -252,19 +248,18 @@ impl<'a> TransactionV1Builder<'a> { /// Returns a new `TransactionV1Builder` suitable for building a transaction for running session /// logic, i.e. compiled Wasm. - pub fn new_session(category: TransactionCategory, module_bytes: Bytes) -> Self { + pub fn new_session(is_install_upgrade: bool, module_bytes: Bytes) -> Self { let target = TransactionTarget::Session { + is_install_upgrade, module_bytes, runtime: Self::DEFAULT_RUNTIME, }; - let body = TransactionV1Body::new( - RuntimeArgs::new(), - target, - TransactionEntryPoint::Call, - category as u8, - Self::DEFAULT_SCHEDULING, - ); - TransactionV1Builder::new(body) + let mut builder = TransactionV1Builder::new(); + builder.args = RuntimeArgs::new(); + builder.target = target; + builder.entry_point = TransactionEntryPoint::Call; + builder.scheduling = Self::DEFAULT_SCHEDULING; + builder } /// Returns a new `TransactionV1Builder` which will build a random, valid but possibly expired @@ -273,23 +268,29 @@ impl<'a> TransactionV1Builder<'a> { /// The transaction can be made invalid in the following ways: /// * unsigned by calling `with_no_secret_key` /// * given an invalid approval by calling `with_invalid_approval` - #[cfg(any(feature = "testing", test))] + #[cfg(any(all(feature = "std", feature = "testing"), test))] pub fn new_random(rng: &mut TestRng) -> Self { let secret_key = SecretKey::random(rng); let ttl_millis = rng.gen_range(60_000..TransactionConfig::default().max_ttl.millis()); - let body = TransactionV1Body::random(rng); + let fields = FieldsContainer::random(rng); TransactionV1Builder { chain_name: Some(rng.random_string(5..10)), timestamp: Timestamp::random(rng), ttl: TimeDiff::from_millis(ttl_millis), - body, + args: RuntimeArgs::random(rng), + target: fields.target, + entry_point: fields.entry_point, + scheduling: fields.scheduling, pricing_mode: PricingMode::Fixed { gas_price_tolerance: 5, + additional_computation_factor: 0, }, initiator_addr: Some(InitiatorAddr::PublicKey(PublicKey::from(&secret_key))), secret_key: Some(secret_key), _phantom_data: PhantomData, invalid_approvals: vec![], + #[cfg(any(all(feature = "std", feature = "testing"), test))] + additional_fields: BTreeMap::new(), } } @@ -299,10 +300,10 @@ impl<'a> TransactionV1Builder<'a> { /// The transaction can be made invalid in the following ways: /// * unsigned by calling `with_no_secret_key` /// * given an invalid approval by calling `with_invalid_approval` - #[cfg(any(feature = "testing", test))] + #[cfg(any(all(feature = "std", feature = "testing"), test))] pub fn new_random_with_category_and_timestamp_and_ttl( rng: &mut TestRng, - category: u8, + lane: u8, timestamp: Option, ttl: Option, ) -> Self { @@ -311,19 +312,30 @@ impl<'a> TransactionV1Builder<'a> { rng.gen_range(60_000..TransactionConfig::default().max_ttl.millis()), |ttl| ttl.millis(), ); - let body = TransactionV1Body::random_of_category(rng, category); + let FieldsContainer { + args, + target, + entry_point, + scheduling, + } = FieldsContainer::random_of_lane(rng, lane); TransactionV1Builder { chain_name: Some(rng.random_string(5..10)), timestamp: timestamp.unwrap_or(Timestamp::now()), ttl: TimeDiff::from_millis(ttl_millis), - body, + args, + target, + entry_point, + scheduling, pricing_mode: PricingMode::Fixed { gas_price_tolerance: 5, + additional_computation_factor: 0, }, initiator_addr: Some(InitiatorAddr::PublicKey(PublicKey::from(&secret_key))), secret_key: Some(secret_key), _phantom_data: PhantomData, invalid_approvals: vec![], + #[cfg(any(all(feature = "std", feature = "testing"), test))] + additional_fields: BTreeMap::new(), } } @@ -377,7 +389,7 @@ impl<'a> TransactionV1Builder<'a> { { self.secret_key = Some(secret_key); } - #[cfg(any(feature = "testing", test))] + #[cfg(any(all(feature = "std", feature = "testing"), test))] { self.secret_key = Some( SecretKey::from_der(secret_key.to_der().expect("should der-encode")) @@ -389,7 +401,7 @@ impl<'a> TransactionV1Builder<'a> { /// Appends the given runtime arg into the body's `args`. pub fn with_runtime_arg>(mut self, key: K, cl_value: CLValue) -> Self { - self.body.args.insert_cl_value(key, cl_value); + self.args.insert_cl_value(key, cl_value); self } @@ -398,7 +410,7 @@ impl<'a> TransactionV1Builder<'a> { /// NOTE: this overwrites any existing runtime args. To append to existing args, use /// [`TransactionV1Builder::with_runtime_arg`]. pub fn with_runtime_args(mut self, args: RuntimeArgs) -> Self { - self.body.args = args; + self.args = args; self } @@ -409,7 +421,7 @@ impl<'a> TransactionV1Builder<'a> { /// NOTE: This has no effect for native transactions, i.e. where the `body.target` is /// `TransactionTarget::Native`. pub fn with_runtime(mut self, runtime: TransactionRuntime) -> Self { - match &mut self.body.target { + match &mut self.target { TransactionTarget::Native => {} TransactionTarget::Stored { runtime: existing_runtime, @@ -431,20 +443,20 @@ impl<'a> TransactionV1Builder<'a> { /// /// If not provided, the scheduling will be set to [`Self::DEFAULT_SCHEDULING`]. pub fn with_scheduling(mut self, scheduling: TransactionScheduling) -> Self { - self.body.scheduling = scheduling; + self.scheduling = scheduling; self } /// Sets the secret key to `None`, meaning the transaction can still be built but will be /// unsigned and will be invalid until subsequently signed. - #[cfg(any(feature = "testing", test))] + #[cfg(any(all(feature = "std", feature = "testing"), test))] pub fn with_no_secret_key(mut self) -> Self { self.secret_key = None; self } /// Sets an invalid approval in the transaction. - #[cfg(any(feature = "testing", test))] + #[cfg(any(all(feature = "std", feature = "testing"), test))] pub fn with_invalid_approval(mut self, rng: &mut TestRng) -> Self { let secret_key = SecretKey::random(rng); let hash = TransactionV1Hash::random(rng).into(); @@ -453,6 +465,13 @@ impl<'a> TransactionV1Builder<'a> { self } + /// Manually sets additional fields + #[cfg(any(all(feature = "std", feature = "testing"), test))] + pub fn with_additional_fields(mut self, additional_fields: BTreeMap) -> Self { + self.additional_fields = additional_fields; + self + } + /// Returns the new transaction, or an error if non-defaulted fields were not set. /// /// For more info, see [the `TransactionBuilder` documentation](TransactionV1Builder). @@ -478,19 +497,28 @@ impl<'a> TransactionV1Builder<'a> { .chain_name .ok_or(TransactionV1BuilderError::MissingChainName)?; + let container = + FieldsContainer::new(self.args, self.target, self.entry_point, self.scheduling) + .to_map() + .map_err(|err| match err { + FieldsContainerError::CouldNotSerializeField { field_index } => { + TransactionV1BuilderError::CouldNotSerializeField { field_index } + } + })?; + let transaction = TransactionV1::build( chain_name, self.timestamp, self.ttl, - self.body, self.pricing_mode, + container, initiator_addr_and_secret_key, ); Ok(transaction) } - #[cfg(any(feature = "testing", test))] + #[cfg(any(all(feature = "std", feature = "testing"), test))] fn do_build(self) -> Result { let initiator_addr_and_secret_key = match (self.initiator_addr, &self.secret_key) { (Some(initiator_addr), Some(secret_key)) => InitiatorAddrAndSecretKey::Both { @@ -507,13 +535,23 @@ impl<'a> TransactionV1Builder<'a> { let chain_name = self .chain_name .ok_or(TransactionV1BuilderError::MissingChainName)?; + let mut container = + FieldsContainer::new(self.args, self.target, self.entry_point, self.scheduling) + .to_map() + .map_err(|err| match err { + FieldsContainerError::CouldNotSerializeField { field_index } => { + TransactionV1BuilderError::CouldNotSerializeField { field_index } + } + })?; + let mut additional_fields = self.additional_fields.clone(); + container.append(&mut additional_fields); let mut transaction = TransactionV1::build( chain_name, self.timestamp, self.ttl, - self.body, self.pricing_mode, + container, initiator_addr_and_secret_key, ); diff --git a/types/src/transaction/transaction_v1/transaction_v1_builder/error.rs b/types/src/transaction/transaction_v1/transaction_v1_builder/error.rs index f92121003f..8ce1196f62 100644 --- a/types/src/transaction/transaction_v1/transaction_v1_builder/error.rs +++ b/types/src/transaction/transaction_v1/transaction_v1_builder/error.rs @@ -19,6 +19,12 @@ pub enum TransactionV1BuilderError { /// Call [`TransactionV1Builder::with_chain_name`] before calling /// [`TransactionV1Builder::build`]. MissingChainName, + /// Failed to build transaction due to an error when calling `to_bytes` on one of the payload + /// `field`. + CouldNotSerializeField { + /// The field index that failed to serialize. + field_index: u16, + }, } impl Display for TransactionV1BuilderError { @@ -36,6 +42,9 @@ impl Display for TransactionV1BuilderError { "transaction requires chain name - use `with_chain_name`" ) } + TransactionV1BuilderError::CouldNotSerializeField { field_index } => { + write!(formatter, "Cannot serialize field at index {}", field_index) + } } } } diff --git a/types/src/transaction/transaction_v1/transaction_v1_category.rs b/types/src/transaction/transaction_v1/transaction_v1_category.rs deleted file mode 100644 index e8b2d8b1bb..0000000000 --- a/types/src/transaction/transaction_v1/transaction_v1_category.rs +++ /dev/null @@ -1,76 +0,0 @@ -use core::{ - convert::TryFrom, - fmt::{self, Formatter}, -}; - -#[cfg(feature = "datasize")] -use datasize::DataSize; -#[cfg(feature = "json-schema")] -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -/// The category of a Transaction. -#[derive( - Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize, Debug, Default, -)] -#[cfg_attr(feature = "datasize", derive(DataSize))] -#[cfg_attr( - feature = "json-schema", - derive(JsonSchema), - schemars(description = "Session kind of a V1 Transaction.") -)] -#[serde(deny_unknown_fields)] -#[repr(u8)] -pub enum TransactionCategory { - /// Native mint interaction (the default). - #[default] - Mint = 0, - /// Native auction interaction. - Auction = 1, - /// Install or Upgrade. - InstallUpgrade = 2, - /// A large Wasm based transaction. - Large = 3, - /// A medium Wasm based transaction. - Medium = 4, - /// A small Wasm based transaction. - Small = 5, -} - -impl fmt::Display for TransactionCategory { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - TransactionCategory::Mint => write!(f, "Mint"), - TransactionCategory::Auction => write!(f, "Auction"), - TransactionCategory::InstallUpgrade => write!(f, "InstallUpgrade"), - TransactionCategory::Large => write!(f, "Large"), - TransactionCategory::Medium => write!(f, "Medium"), - TransactionCategory::Small => write!(f, "Small"), - } - } -} - -#[derive(Debug)] -pub struct TransactionCategoryConversionError(u8); - -impl fmt::Display for TransactionCategoryConversionError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Failed to convert {} into a TransactionCategory", self.0) - } -} - -impl TryFrom for TransactionCategory { - type Error = TransactionCategoryConversionError; - - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(Self::Mint), - 1 => Ok(Self::Auction), - 2 => Ok(Self::InstallUpgrade), - 3 => Ok(Self::Large), - 4 => Ok(Self::Medium), - 5 => Ok(Self::Small), - _ => Err(TransactionCategoryConversionError(value)), - } - } -} diff --git a/types/src/transaction/transaction_v1/transaction_v1_header.rs b/types/src/transaction/transaction_v1/transaction_v1_header.rs deleted file mode 100644 index 9755f4f00b..0000000000 --- a/types/src/transaction/transaction_v1/transaction_v1_header.rs +++ /dev/null @@ -1,271 +0,0 @@ -use alloc::{string::String, vec::Vec}; -use core::fmt::{self, Display, Formatter}; - -#[cfg(feature = "datasize")] -use datasize::DataSize; -#[cfg(feature = "json-schema")] -use schemars::JsonSchema; -#[cfg(any(feature = "std", test))] -use serde::{Deserialize, Serialize}; -#[cfg(any(feature = "std", test))] -use tracing::debug; - -#[cfg(doc)] -use super::TransactionV1; -use super::{InitiatorAddr, PricingMode}; -use crate::{ - bytesrepr::{ - Error::{self, Formatting}, - FromBytes, ToBytes, - }, - transaction::serialization::{ - CalltableSerializationEnvelope, CalltableSerializationEnvelopeBuilder, - }, - Digest, TimeDiff, Timestamp, -}; -#[cfg(any(feature = "std", test))] -use crate::{InvalidTransactionV1, TransactionConfig, TransactionV1Hash}; - -const CHAIN_NAME_INDEX: u16 = 0; -const TIMESTAMP_INDEX: u16 = 1; -const TTL_INDEX: u16 = 2; -const BODY_HASH_INDEX: u16 = 3; -const PRICING_MODE_INDEX: u16 = 4; -const INITIATOR_ADDR_INDEX: u16 = 5; - -/// The header portion of a [`TransactionV1`]. -#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)] -#[cfg_attr( - any(feature = "std", test), - derive(Serialize, Deserialize), - serde(deny_unknown_fields) -)] -#[cfg_attr(feature = "datasize", derive(DataSize))] -#[cfg_attr( - feature = "json-schema", - derive(JsonSchema), - schemars(description = "The header portion of a TransactionV1.") -)] -pub struct TransactionV1Header { - chain_name: String, - timestamp: Timestamp, - ttl: TimeDiff, - body_hash: Digest, - pricing_mode: PricingMode, - initiator_addr: InitiatorAddr, -} - -impl TransactionV1Header { - fn serialized_field_lengths(&self) -> Vec { - vec![ - self.chain_name.serialized_length(), - self.timestamp.serialized_length(), - self.ttl.serialized_length(), - self.body_hash.serialized_length(), - self.pricing_mode.serialized_length(), - self.initiator_addr.serialized_length(), - ] - } - - #[cfg(any(feature = "std", feature = "json-schema", test))] - pub(super) fn new( - chain_name: String, - timestamp: Timestamp, - ttl: TimeDiff, - body_hash: Digest, - pricing_mode: PricingMode, - initiator_addr: InitiatorAddr, - ) -> Self { - TransactionV1Header { - chain_name, - timestamp, - ttl, - body_hash, - pricing_mode, - initiator_addr, - } - } - - /// Computes the hash identifying this transaction. - #[cfg(any(feature = "std", test))] - pub fn compute_hash(&self) -> TransactionV1Hash { - TransactionV1Hash::new(Digest::hash( - self.to_bytes() - .unwrap_or_else(|error| panic!("should serialize header: {}", error)), - )) - } - - /// Returns the name of the chain the transaction should be executed on. - pub fn chain_name(&self) -> &str { - &self.chain_name - } - - /// Returns the creation timestamp of the transaction. - pub fn timestamp(&self) -> Timestamp { - self.timestamp - } - - /// Returns the duration after the creation timestamp for which the transaction will stay valid. - /// - /// After this duration has ended, the transaction will be considered expired. - pub fn ttl(&self) -> TimeDiff { - self.ttl - } - - /// Returns `true` if the transaction has expired. - pub fn expired(&self, current_instant: Timestamp) -> bool { - self.expires() < current_instant - } - - /// Returns the hash of the body of the transaction. - pub fn body_hash(&self) -> &Digest { - &self.body_hash - } - - /// Returns the pricing mode for the transaction. - pub fn pricing_mode(&self) -> &PricingMode { - &self.pricing_mode - } - - /// Returns the address of the initiator of the transaction. - pub fn initiator_addr(&self) -> &InitiatorAddr { - &self.initiator_addr - } - - /// Returns `Ok` if and only if the TTL is within limits, and the timestamp is not later than - /// `at + timestamp_leeway`. Does NOT check for expiry. - #[cfg(any(feature = "std", test))] - pub fn is_valid( - &self, - config: &TransactionConfig, - timestamp_leeway: TimeDiff, - at: Timestamp, - transaction_hash: &TransactionV1Hash, - ) -> Result<(), InvalidTransactionV1> { - if self.ttl() > config.max_ttl { - debug!( - %transaction_hash, - transaction_header = %self, - max_ttl = %config.max_ttl, - "transaction ttl excessive" - ); - return Err(InvalidTransactionV1::ExcessiveTimeToLive { - max_ttl: config.max_ttl, - got: self.ttl(), - }); - } - - if self.timestamp() > at + timestamp_leeway { - debug!( - %transaction_hash, transaction_header = %self, %at, - "transaction timestamp in the future" - ); - return Err(InvalidTransactionV1::TimestampInFuture { - validation_timestamp: at, - timestamp_leeway, - got: self.timestamp(), - }); - } - - Ok(()) - } - - /// Returns the timestamp of when the transaction expires, i.e. `self.timestamp + self.ttl`. - pub fn expires(&self) -> Timestamp { - self.timestamp.saturating_add(self.ttl) - } - - /// Returns the gas price tolerance for the given transaction. - pub fn gas_price_tolerance(&self) -> u8 { - match self.pricing_mode { - PricingMode::Classic { - gas_price_tolerance, - .. - } => gas_price_tolerance, - PricingMode::Fixed { - gas_price_tolerance, - .. - } => gas_price_tolerance, - PricingMode::Reserved { .. } => { - // TODO: Change this when reserve gets implemented. - 0u8 - } - } - } - - #[cfg(any(all(feature = "std", feature = "testing"), test))] - pub(super) fn invalidate(&mut self) { - self.chain_name.clear(); - } -} - -impl ToBytes for TransactionV1Header { - fn to_bytes(&self) -> Result, Error> { - CalltableSerializationEnvelopeBuilder::new(self.serialized_field_lengths())? - .add_field(CHAIN_NAME_INDEX, &self.chain_name)? - .add_field(TIMESTAMP_INDEX, &self.timestamp)? - .add_field(TTL_INDEX, &self.ttl)? - .add_field(BODY_HASH_INDEX, &self.body_hash)? - .add_field(PRICING_MODE_INDEX, &self.pricing_mode)? - .add_field(INITIATOR_ADDR_INDEX, &self.initiator_addr)? - .binary_payload_bytes() - } - fn serialized_length(&self) -> usize { - CalltableSerializationEnvelope::estimate_size(self.serialized_field_lengths()) - } -} - -impl FromBytes for TransactionV1Header { - fn from_bytes(bytes: &[u8]) -> Result<(TransactionV1Header, &[u8]), Error> { - let (binary_payload, remainder) = - crate::transaction::serialization::CalltableSerializationEnvelope::from_bytes( - 6u32, bytes, - )?; - let window = binary_payload.start_consuming()?; - let window = window.ok_or(Formatting)?; - window.verify_index(CHAIN_NAME_INDEX)?; - let (chain_name, window) = window.deserialize_and_maybe_next::()?; - let window = window.ok_or(Formatting)?; - window.verify_index(TIMESTAMP_INDEX)?; - let (timestamp, window) = window.deserialize_and_maybe_next::()?; - let window = window.ok_or(Formatting)?; - window.verify_index(TTL_INDEX)?; - let (ttl, window) = window.deserialize_and_maybe_next::()?; - let window = window.ok_or(Formatting)?; - window.verify_index(BODY_HASH_INDEX)?; - let (body_hash, window) = window.deserialize_and_maybe_next::()?; - let window = window.ok_or(Formatting)?; - window.verify_index(PRICING_MODE_INDEX)?; - let (pricing_mode, window) = window.deserialize_and_maybe_next::()?; - let window = window.ok_or(Formatting)?; - window.verify_index(INITIATOR_ADDR_INDEX)?; - let (initiator_addr, window) = window.deserialize_and_maybe_next::()?; - if window.is_some() { - return Err(Formatting); - } - let from_bytes = TransactionV1Header { - chain_name, - timestamp, - ttl, - body_hash, - pricing_mode, - initiator_addr, - }; - Ok((from_bytes, remainder)) - } -} - -impl Display for TransactionV1Header { - fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { - #[cfg(any(feature = "std", test))] - let hash = self.compute_hash(); - #[cfg(not(any(feature = "std", test)))] - let hash = "unknown"; - write!( - formatter, - "transaction-v1-header[{}, chain_name: {}, timestamp: {}, ttl: {}, pricing mode: {}, \ - initiator: {}]", - hash, self.chain_name, self.timestamp, self.ttl, self.pricing_mode, self.initiator_addr - ) - } -} diff --git a/types/src/transaction/transaction_v1/transaction_v1_payload.rs b/types/src/transaction/transaction_v1/transaction_v1_payload.rs new file mode 100644 index 0000000000..d54f87e5c0 --- /dev/null +++ b/types/src/transaction/transaction_v1/transaction_v1_payload.rs @@ -0,0 +1,469 @@ +use core::fmt::{self, Debug, Display, Formatter}; + +use super::{errors_v1::FieldDeserializationError, PricingMode}; +use crate::{ + bytesrepr::{ + Bytes, + Error::{self, Formatting}, + FromBytes, ToBytes, + }, + transaction::serialization::{ + CalltableSerializationEnvelope, CalltableSerializationEnvelopeBuilder, + }, + DisplayIter, InitiatorAddr, TimeDiff, Timestamp, +}; +use alloc::{collections::BTreeMap, string::String, vec::Vec}; +#[cfg(feature = "datasize")] +use datasize::DataSize; +#[cfg(feature = "json-schema")] +use schemars::JsonSchema; +#[cfg(any(feature = "std", test))] +use serde::{Deserialize, Serialize}; + +const INITIATOR_ADDR_FIELD_INDEX: u16 = 0; +const TIMESTAMP_FIELD_INDEX: u16 = 1; +const TTL_FIELD_INDEX: u16 = 2; +const CHAIN_NAME_FIELD_INDEX: u16 = 3; +const PRICING_MODE_FIELD_INDEX: u16 = 4; +const FIELDS_FIELD_INDEX: u16 = 5; + +const ARGS_MAP_KEY: u16 = 0; +const TARGET_MAP_KEY: u16 = 1; +const ENTRY_POINT_MAP_KEY: u16 = 2; +const SCHEDULING_MAP_KEY: u16 = 3; +const EXPECTED_FIELD_KEYS: [u16; 4] = [ + ARGS_MAP_KEY, + TARGET_MAP_KEY, + ENTRY_POINT_MAP_KEY, + SCHEDULING_MAP_KEY, +]; + +#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)] +#[cfg_attr( + any(feature = "std", test), + derive(Serialize, Deserialize), + serde(deny_unknown_fields) +)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr( + feature = "json-schema", + derive(JsonSchema), + schemars( + description = "A unit of work sent by a client to the network, which when executed can \ + cause global state to be altered." + ) +)] +pub struct TransactionV1Payload { + initiator_addr: InitiatorAddr, + timestamp: Timestamp, + ttl: TimeDiff, + chain_name: String, + pricing_mode: PricingMode, + fields: BTreeMap, +} + +impl TransactionV1Payload { + pub fn new( + chain_name: String, + timestamp: Timestamp, + ttl: TimeDiff, + pricing_mode: PricingMode, + initiator_addr: InitiatorAddr, + fields: BTreeMap, + ) -> TransactionV1Payload { + TransactionV1Payload { + chain_name, + timestamp, + ttl, + pricing_mode, + initiator_addr, + fields, + } + } + + fn serialized_field_lengths(&self) -> Vec { + vec![ + self.initiator_addr.serialized_length(), + self.timestamp.serialized_length(), + self.ttl.serialized_length(), + self.chain_name.serialized_length(), + self.pricing_mode.serialized_length(), + self.fields.serialized_length(), + ] + } + + pub fn chain_name(&self) -> &str { + &self.chain_name + } + + pub fn timestamp(&self) -> Timestamp { + self.timestamp + } + + pub fn ttl(&self) -> TimeDiff { + self.ttl + } + + pub fn pricing_mode(&self) -> &PricingMode { + &self.pricing_mode + } + + pub fn initiator_addr(&self) -> &InitiatorAddr { + &self.initiator_addr + } + + pub fn fields(&self) -> &BTreeMap { + &self.fields + } + + /// Returns the timestamp of when the transaction expires, i.e. `self.timestamp + self.ttl`. + pub fn expires(&self) -> Timestamp { + self.timestamp.saturating_add(self.ttl) + } + + /// Returns `true` if the transaction has expired. + pub fn expired(&self, current_instant: Timestamp) -> bool { + self.expires() < current_instant + } + + pub fn deserialize_field( + &self, + index: u16, + ) -> Result { + let field = self + .fields + .get(&index) + .ok_or(FieldDeserializationError::IndexNotExists { index })?; + let (value, remainder) = T::from_bytes(field) + .map_err(|error| FieldDeserializationError::FromBytesError { index, error })?; + if !remainder.is_empty() { + return Err(FieldDeserializationError::LingeringBytesInField { index }); + } + Ok(value) + } + + pub fn number_of_fields(&self) -> usize { + self.fields.len() + } + + #[cfg(any(all(feature = "std", feature = "testing"), test))] + pub fn invalidate(&mut self) { + self.chain_name.clear(); + } +} + +impl ToBytes for TransactionV1Payload { + fn to_bytes(&self) -> Result, crate::bytesrepr::Error> { + let expected_payload_sizes = self.serialized_field_lengths(); + CalltableSerializationEnvelopeBuilder::new(expected_payload_sizes)? + .add_field(INITIATOR_ADDR_FIELD_INDEX, &self.initiator_addr)? + .add_field(TIMESTAMP_FIELD_INDEX, &self.timestamp)? + .add_field(TTL_FIELD_INDEX, &self.ttl)? + .add_field(CHAIN_NAME_FIELD_INDEX, &self.chain_name)? + .add_field(PRICING_MODE_FIELD_INDEX, &self.pricing_mode)? + .add_field(FIELDS_FIELD_INDEX, &self.fields)? + .binary_payload_bytes() + } + + fn serialized_length(&self) -> usize { + CalltableSerializationEnvelope::estimate_size(self.serialized_field_lengths()) + } +} + +impl FromBytes for TransactionV1Payload { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), Error> { + let (binary_payload, remainder) = CalltableSerializationEnvelope::from_bytes(6, bytes)?; + let window = binary_payload.start_consuming()?.ok_or(Formatting)?; + + window.verify_index(INITIATOR_ADDR_FIELD_INDEX)?; + let (initiator_addr, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(Formatting)?; + window.verify_index(TIMESTAMP_FIELD_INDEX)?; + let (timestamp, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(Formatting)?; + window.verify_index(TTL_FIELD_INDEX)?; + let (ttl, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(Formatting)?; + window.verify_index(CHAIN_NAME_FIELD_INDEX)?; + let (chain_name, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(Formatting)?; + window.verify_index(PRICING_MODE_FIELD_INDEX)?; + let (pricing_mode, window) = window.deserialize_and_maybe_next::()?; + let window = window.ok_or(Formatting)?; + window.verify_index(FIELDS_FIELD_INDEX)?; + let (fields_as_vec, window) = window.deserialize_and_maybe_next::>()?; + let fields = build_map(fields_as_vec)?; + if window.is_some() { + return Err(Formatting); + } + if fields.len() != EXPECTED_FIELD_KEYS.len() + || EXPECTED_FIELD_KEYS + .iter() + .any(|expected_key| !fields.contains_key(expected_key)) + { + return Err(Formatting); + } + let from_bytes = TransactionV1Payload { + chain_name, + timestamp, + ttl, + pricing_mode, + initiator_addr, + fields, + }; + + Ok((from_bytes, remainder)) + } +} + +// We need to make sure that the bytes of the `fields` field are serialized in the correct order. +// A BTreeMap is serialized the same as Vec<(K, V)> and it actually, on deserialization, doesn't +// check if the keys are in ascending order. We need to make sure that the incoming transaction +// payload is serialized in a strict way, otherwise we would have trouble with verifying the +// signature(s). +fn build_map(fields_as_vec: Vec<(u16, Bytes)>) -> Result, Error> { + let mut ret = BTreeMap::new(); + let mut max_idx: i32 = -1; + for (key, value) in fields_as_vec { + let key_signed = key as i32; + if key_signed <= max_idx { + return Err(Formatting); + } + max_idx = key_signed; + ret.insert(key, value); + } + + Ok(ret) +} + +impl Display for TransactionV1Payload { + fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { + write!( + formatter, + "transaction-v1-payload[{}, {}, {}, {}, {}, fields: {}]", + self.chain_name, + self.timestamp, + self.ttl, + self.pricing_mode, + self.initiator_addr, + DisplayIter::new(self.fields.keys()) + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + testing::TestRng, RuntimeArgs, TransactionEntryPoint, TransactionScheduling, + TransactionTarget, + }; + use std::collections::BTreeMap; + + #[test] + fn reserialize_should_work_with_ascending_ids() { + let input = vec![ + (0, Bytes::from(vec![1])), + (1, Bytes::from(vec![2])), + (4, Bytes::from(vec![3])), + ]; + let map = build_map(input).expect("Should not fail"); + assert_eq!( + map, + BTreeMap::from_iter(vec![ + (0, Bytes::from(vec![1])), + (1, Bytes::from(vec![2])), + (4, Bytes::from(vec![3])) + ]) + ); + } + + #[test] + fn reserialize_should_fail_when_ids_not_unique() { + let input = vec![ + (0, Bytes::from(vec![1])), + (0, Bytes::from(vec![2])), + (4, Bytes::from(vec![3])), + ]; + let map_ret = build_map(input); + assert!(map_ret.is_err()); + } + + #[test] + fn reserialize_should_fail_when_ids_not_ascending() { + let input = vec![ + (0, Bytes::from(vec![1])), + (2, Bytes::from(vec![2])), + (1, Bytes::from(vec![3])), + ]; + assert!(build_map(input).is_err()); + let input = vec![ + (0, Bytes::from(vec![1])), + (2, Bytes::from(vec![2])), + (0, Bytes::from(vec![3])), + ]; + assert!(build_map(input).is_err()); + let input = vec![ + (0, Bytes::from(vec![1])), + (1, Bytes::from(vec![2])), + (2, Bytes::from(vec![3])), + (3, Bytes::from(vec![4])), + (2, Bytes::from(vec![5])), + ]; + assert!(build_map(input).is_err()); + } + + #[test] + fn should_fail_if_deserialized_payload_has_too_many_fields() { + let rng = &mut TestRng::new(); + let ( + args, + target, + entry_point, + scheduling, + initiator_addr, + timestamp, + ttl, + chain_name, + pricing_mode, + ) = random_payload_data(rng); + let mut fields = BTreeMap::new(); + fields.insert(ARGS_MAP_KEY, args.to_bytes().unwrap().into()); + fields.insert(TARGET_MAP_KEY, target.to_bytes().unwrap().into()); + fields.insert(ENTRY_POINT_MAP_KEY, entry_point.to_bytes().unwrap().into()); + fields.insert(SCHEDULING_MAP_KEY, scheduling.to_bytes().unwrap().into()); + fields.insert(4, 111_u64.to_bytes().unwrap().into()); + + let bytes = TransactionV1Payload::new( + chain_name, + timestamp, + ttl, + pricing_mode, + initiator_addr, + fields, + ) + .to_bytes() + .unwrap(); + let result = TransactionV1Payload::from_bytes(&bytes); + assert!(result.is_err()); + } + + #[test] + fn should_fail_if_deserialized_payload_has_unrecognized_fields() { + let rng = &mut TestRng::new(); + let ( + args, + target, + entry_point, + scheduling, + initiator_addr, + timestamp, + ttl, + chain_name, + pricing_mode, + ) = random_payload_data(rng); + let mut fields = BTreeMap::new(); + fields.insert(ARGS_MAP_KEY, args.to_bytes().unwrap().into()); + fields.insert(TARGET_MAP_KEY, target.to_bytes().unwrap().into()); + fields.insert(100, entry_point.to_bytes().unwrap().into()); + fields.insert(SCHEDULING_MAP_KEY, scheduling.to_bytes().unwrap().into()); + + let bytes = TransactionV1Payload::new( + chain_name, + timestamp, + ttl, + pricing_mode, + initiator_addr, + fields, + ) + .to_bytes() + .unwrap(); + let result = TransactionV1Payload::from_bytes(&bytes); + assert!(result.is_err()); + } + + #[test] + fn should_fail_if_serialized_payoad_has_fields_out_of_order() { + let rng = &mut TestRng::new(); + let ( + args, + target, + entry_point, + scheduling, + initiator_addr, + timestamp, + ttl, + chain_name, + pricing_mode, + ) = random_payload_data(rng); + let fields: Vec<(u16, Bytes)> = vec![ + (SCHEDULING_MAP_KEY, scheduling.to_bytes().unwrap().into()), + (TARGET_MAP_KEY, target.to_bytes().unwrap().into()), + (ENTRY_POINT_MAP_KEY, entry_point.to_bytes().unwrap().into()), + (ARGS_MAP_KEY, args.to_bytes().unwrap().into()), + ]; + + let expected_payload_sizes = vec![ + initiator_addr.serialized_length(), + timestamp.serialized_length(), + ttl.serialized_length(), + chain_name.serialized_length(), + pricing_mode.serialized_length(), + fields.serialized_length(), + ]; + + let bytes = CalltableSerializationEnvelopeBuilder::new(expected_payload_sizes) + .unwrap() + .add_field(INITIATOR_ADDR_FIELD_INDEX, &initiator_addr) + .unwrap() + .add_field(TIMESTAMP_FIELD_INDEX, ×tamp) + .unwrap() + .add_field(TTL_FIELD_INDEX, &ttl) + .unwrap() + .add_field(CHAIN_NAME_FIELD_INDEX, &chain_name) + .unwrap() + .add_field(PRICING_MODE_FIELD_INDEX, &pricing_mode) + .unwrap() + .add_field(FIELDS_FIELD_INDEX, &fields) + .unwrap() + .binary_payload_bytes() + .unwrap(); + let payload_res = TransactionV1Payload::from_bytes(&bytes); + assert!(payload_res.is_err()); + } + + fn random_payload_data( + rng: &mut TestRng, + ) -> ( + RuntimeArgs, + TransactionTarget, + TransactionEntryPoint, + TransactionScheduling, + InitiatorAddr, + Timestamp, + TimeDiff, + String, + PricingMode, + ) { + let args = RuntimeArgs::random(rng); + let target = TransactionTarget::random(rng); + let entry_point = TransactionEntryPoint::random(rng); + let scheduling = TransactionScheduling::random(rng); + let initiator_addr = InitiatorAddr::random(rng); + let timestamp = Timestamp::now(); + let ttl = TimeDiff::from_millis(1000); + let chain_name = "chain-name".to_string(); + let pricing_mode = PricingMode::random(rng); + ( + args, + target, + entry_point, + scheduling, + initiator_addr, + timestamp, + ttl, + chain_name, + pricing_mode, + ) + } +}