From 6fe183d0412aacd1f8f09e9dd3439df97141b99f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Thu, 4 Apr 2024 08:51:43 +0200 Subject: [PATCH 1/6] Integrate reduced costs execution. --- concordium-rust-sdk | 2 +- contract-testing/src/impls.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/concordium-rust-sdk b/concordium-rust-sdk index 6eb5f3b7..7a4f9bde 160000 --- a/concordium-rust-sdk +++ b/concordium-rust-sdk @@ -1 +1 @@ -Subproject commit 6eb5f3b768256b1e77d6d4340e121ead4a40aa20 +Subproject commit 7a4f9bde9e425d48483d9bf82ef54f0e6688b3a1 diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index 43158ad4..c21504b4 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -31,7 +31,7 @@ use concordium_rust_sdk::{ }; use num_bigint::BigUint; use num_integer::Integer; -use sdk::types::smart_contracts::InvokeContractResult; +use sdk::{types::smart_contracts::InvokeContractResult, smart_contracts::engine::wasm::CostConfigurationV1}; use std::{ collections::{BTreeMap, BTreeSet}, future::Future, @@ -584,6 +584,7 @@ impl Chain { // Construct the artifact. let artifact = match wasm::utils::instantiate_with_metering::( ValidationConfig::V1, + CostConfigurationV1, &v1::ConcordiumAllowedImports { support_upgrade: true, enable_debug, From b046083563f80604f310c5639e6e4e96eabbbb2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Sat, 6 Apr 2024 20:24:03 +0200 Subject: [PATCH 2/6] Keep track of module lookup costs. --- concordium-rust-sdk | 2 +- contract-testing/src/impls.rs | 156 ++++++++++++++--------- contract-testing/src/invocation/impls.rs | 8 +- contract-testing/src/invocation/types.rs | 2 + contract-testing/src/types.rs | 4 + 5 files changed, 111 insertions(+), 61 deletions(-) diff --git a/concordium-rust-sdk b/concordium-rust-sdk index 7a4f9bde..06bf23bf 160000 --- a/concordium-rust-sdk +++ b/concordium-rust-sdk @@ -1 +1 @@ -Subproject commit 7a4f9bde9e425d48483d9bf82ef54f0e6688b3a1 +Subproject commit 06bf23bf12ad281c1f66913a1b7db066e3827a96 diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index c21504b4..a16643d1 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -31,7 +31,10 @@ use concordium_rust_sdk::{ }; use num_bigint::BigUint; use num_integer::Integer; -use sdk::{types::smart_contracts::InvokeContractResult, smart_contracts::engine::wasm::CostConfigurationV1}; +use sdk::{ + smart_contracts::engine::wasm::CostConfigurationV1, + types::smart_contracts::InvokeContractResult, +}; use std::{ collections::{BTreeMap, BTreeSet}, future::Future, @@ -919,7 +922,8 @@ impl Chain { amount_reserved_for_energy: Amount, payload: UpdateContractPayload, remaining_energy: &mut Energy, - ) -> Result<(InvokeResponse, ChangeSet, Vec), ContractInvokeError> { + ) -> Result<(InvokeResponse, ChangeSet, Vec, Energy), ContractInvokeError> + { // Check if the contract to invoke exists. if !self.contract_exists(payload.address) { return Err(self.convert_to_invoke_error( @@ -930,6 +934,7 @@ impl Chain { Vec::new(), energy_reserved, *remaining_energy, + 0.into(), )); } @@ -940,6 +945,7 @@ impl Chain { Vec::new(), energy_reserved, *remaining_energy, + 0.into(), )); } @@ -957,6 +963,7 @@ impl Chain { Vec::new(), energy_reserved, *remaining_energy, + 0.into(), )); } @@ -970,18 +977,23 @@ impl Chain { // Starts at 1 since 0 is the "initial state" of all contracts in the current // transaction. next_contract_modification_index: 1, + module_load_energy: 0.into(), }; - + let module_load_energy = contract_invocation.module_load_energy; let res = contract_invocation.invoke_entrypoint(invoker, sender, payload); match res { - Ok((result, trace_elements)) => { - Ok((result, contract_invocation.changeset, trace_elements)) - } + Ok((result, trace_elements)) => Ok(( + result, + contract_invocation.changeset, + trace_elements, + contract_invocation.module_load_energy, + )), Err(err) => Err(self.convert_to_invoke_error( err.into(), Vec::new(), energy_reserved, *remaining_energy, + module_load_energy, )), } } @@ -992,7 +1004,9 @@ impl Chain { trace_elements: Vec, energy_reserved: Energy, remaining_energy: Energy, + state_energy: Energy, state_changed: bool, + module_load_energy: Energy, ) -> Result { match result { v1::InvokeResponse::Success { @@ -1008,6 +1022,8 @@ impl Chain { return_value: data.unwrap_or_default(), state_changed, new_balance, + storage_energy: state_energy, + module_load_energy, }) } v1::InvokeResponse::Failure { @@ -1019,6 +1035,7 @@ impl Chain { trace_elements, energy_reserved, remaining_energy, + module_load_energy, )), } } @@ -1053,10 +1070,11 @@ impl Chain { // is verified upfront. So what we do here is custom behaviour, and we reject // without consuming any energy. return Err(ContractInvokeError { - energy_used: Energy::from(0), - transaction_fee: Amount::zero(), - trace_elements: Vec::new(), - kind: ContractInvokeErrorKind::SenderDoesNotExist(sender), + energy_used: Energy::from(0), + transaction_fee: Amount::zero(), + trace_elements: Vec::new(), + kind: ContractInvokeErrorKind::SenderDoesNotExist(sender), + module_load_energy: 0.into(), }); } @@ -1069,6 +1087,7 @@ impl Chain { kind: ContractInvokeErrorKind::InvokerDoesNotExist( AccountDoesNotExist { address: invoker }, ), + module_load_energy: 0.into(), }); }; @@ -1083,12 +1102,13 @@ impl Chain { // Charge the header cost. let mut remaining_energy = energy_reserved.checked_sub(check_header_cost).ok_or(ContractInvokeError { - energy_used: Energy::from(0), - transaction_fee: Amount::zero(), - trace_elements: Vec::new(), - kind: ContractInvokeErrorKind::OutOfEnergy { + energy_used: Energy::from(0), + transaction_fee: Amount::zero(), + trace_elements: Vec::new(), + kind: ContractInvokeErrorKind::OutOfEnergy { debug_trace: DebugTracker::empty_trace(), // we haven't done anything yet. }, + module_load_energy: 0.into(), })?; let invoker_amount_reserved_for_nrg = @@ -1102,6 +1122,7 @@ impl Chain { transaction_fee: self.parameters.calculate_energy_cost(energy_used), trace_elements: Vec::new(), kind: ContractInvokeErrorKind::InsufficientFunds, + module_load_energy: 0.into(), }); } @@ -1115,37 +1136,43 @@ impl Chain { &mut remaining_energy, ); let res = match res { - Ok((result, changeset, trace_elements)) => { + Ok((result, changeset, trace_elements, module_load_energy)) => { // Charge energy for contract storage. Or return an error if out // of energy. - let state_changed = if matches!(result, v1::InvokeResponse::Success { .. }) { - let res = changeset.persist( - &mut remaining_energy, - contract_address, - &mut self.accounts, - &mut self.contracts, - ); - if let Ok(res) = res { - res + let (state_energy, state_changed) = + if matches!(result, v1::InvokeResponse::Success { .. }) { + let energy_before = remaining_energy; + let res = changeset.persist( + &mut remaining_energy, + contract_address, + &mut self.accounts, + &mut self.contracts, + ); + let state_energy = energy_before.checked_sub(remaining_energy).unwrap(); + if let Ok(res) = res { + (state_energy, res) + } else { + // the error happens when storing the state, so there are no trace + // elements associated with it. The trace is + // already in the "debug trace" vector. + return Err(self.invocation_out_of_energy_error( + energy_reserved, + DebugTracker::empty_trace(), + module_load_energy, + )); + } } else { - // the error happens when storing the state, so there are no trace elements - // associated with it. The trace is already in the - // "debug trace" vector. - return Err(self.invocation_out_of_energy_error( - energy_reserved, - DebugTracker::empty_trace(), - )); - } - } else { - // An error occurred, so state hasn't changed. - false - }; + // An error occurred, so state hasn't changed. + (0.into(), false) + }; self.contract_invocation_process_response( result, trace_elements, energy_reserved, remaining_energy, + state_energy, state_changed, + module_load_energy, ) } Err(e) => Err(e), @@ -1185,10 +1212,11 @@ impl Chain { // Ensure the sender exists. if !self.address_exists(sender) { return Err(ContractInvokeError { - energy_used: Energy::from(0), - transaction_fee: Amount::zero(), - trace_elements: Vec::new(), - kind: ContractInvokeErrorKind::SenderDoesNotExist(sender), + energy_used: Energy::from(0), + transaction_fee: Amount::zero(), + trace_elements: Vec::new(), + kind: ContractInvokeErrorKind::SenderDoesNotExist(sender), + module_load_energy: 0.into(), }); } @@ -1200,6 +1228,7 @@ impl Chain { kind: ContractInvokeErrorKind::InvokerDoesNotExist( AccountDoesNotExist { address: invoker }, ), + module_load_energy: 0.into(), }); }; @@ -1213,6 +1242,7 @@ impl Chain { transaction_fee: self.parameters.calculate_energy_cost(energy_used), trace_elements: Vec::new(), kind: ContractInvokeErrorKind::InsufficientFunds, + module_load_energy: 0.into(), }); } @@ -1229,33 +1259,39 @@ impl Chain { &mut remaining_energy, ); match res { - Ok((result, changeset, trace_elements)) => { + Ok((result, changeset, trace_elements, module_load_energy)) => { // Charge energy for contract storage. Or return an error if out // of energy. - let state_changed = if matches!(result, v1::InvokeResponse::Success { .. }) { - if let Ok(state_changed) = - changeset.collect_energy_for_state(&mut remaining_energy, contract_address) - { - state_changed + let (state_energy, state_changed) = + if matches!(result, v1::InvokeResponse::Success { .. }) { + let energy_before = remaining_energy; + if let Ok(state_changed) = changeset + .collect_energy_for_state(&mut remaining_energy, contract_address) + { + let state_energy = energy_before.checked_sub(remaining_energy).unwrap(); + (state_energy, state_changed) + } else { + // the error happens when storing the state, so there are no trace + // elements associated with it. The trace is + // already in the "debug trace" vector. + return Err(self.invocation_out_of_energy_error( + energy_reserved, + DebugTracker::empty_trace(), + module_load_energy, + )); + } } else { - // the error happens when storing the state, so there are no trace elements - // associated with it. The trace is already in the - // "debug trace" vector. - return Err(self.invocation_out_of_energy_error( - energy_reserved, - DebugTracker::empty_trace(), - )); - } - } else { - // An error occurred, so state hasn't changed. - false - }; + // An error occurred, so state hasn't changed. + (0.into(), false) + }; self.contract_invocation_process_response( result, trace_elements, energy_reserved, remaining_energy, + state_energy, state_changed, + module_load_energy, ) } Err(e) => Err(e), @@ -1552,6 +1588,7 @@ impl Chain { trace_elements: Vec, energy_reserved: Energy, remaining_energy: Energy, + module_load_energy: Energy, ) -> ContractInvokeError { let remaining_energy = if matches!(kind, ContractInvokeErrorKind::OutOfEnergy { .. }) { 0.into() @@ -1565,6 +1602,7 @@ impl Chain { transaction_fee, trace_elements, kind, + module_load_energy, } } @@ -1575,6 +1613,7 @@ impl Chain { &self, energy_reserved: Energy, debug_trace: DebugTracker, + module_load_energy: Energy, ) -> ContractInvokeError { self.convert_to_invoke_error( ContractInvokeErrorKind::OutOfEnergy { @@ -1583,6 +1622,7 @@ impl Chain { Vec::new(), energy_reserved, Energy::from(0), + module_load_energy, ) } diff --git a/contract-testing/src/invocation/impls.rs b/contract-testing/src/invocation/impls.rs index 6eda94d9..3e1d795d 100644 --- a/contract-testing/src/invocation/impls.rs +++ b/contract-testing/src/invocation/impls.rs @@ -174,10 +174,12 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { }; // Subtract the cost of looking up the module + let lookup_costs = lookup_module_cost(&module); exit_ooe!( - self.remaining_energy.tick_energy(lookup_module_cost(&module)), + self.remaining_energy.tick_energy(lookup_costs), DebugTracker::empty_trace() ); + self.module_load_energy.energy += lookup_costs.energy; // Sender policies have a very bespoke serialization in // order to allow skipping portions of them in smart contracts. @@ -653,9 +655,11 @@ impl<'a, 'b> EntrypointInvocationHandler<'a, 'b> { }, Some(module) => { // Charge for the module lookup. + let lookup_costs = lookup_module_cost(module); + self.module_load_energy.energy += lookup_costs.energy; exit_ooe!( self.remaining_energy - .tick_energy(lookup_module_cost(module)), + .tick_energy(lookup_costs), DebugTracker::empty_trace() ); diff --git a/contract-testing/src/invocation/types.rs b/contract-testing/src/invocation/types.rs index 1470d5c1..2bde9ae0 100644 --- a/contract-testing/src/invocation/types.rs +++ b/contract-testing/src/invocation/types.rs @@ -33,6 +33,8 @@ pub(crate) struct EntrypointInvocationHandler<'a, 'b> { pub(crate) changeset: ChangeSet, /// The energy remaining for execution. pub(crate) remaining_energy: &'a mut Energy, + /// Costs incurred from smart contract module loading. + pub(crate) module_load_energy: Energy, /// The energy reserved for the execution. Used for calculating intermediate /// energy usages in contract trace elements. pub(crate) energy_reserved: Energy, diff --git a/contract-testing/src/types.rs b/contract-testing/src/types.rs index d6a4e469..10a3d0f5 100644 --- a/contract-testing/src/types.rs +++ b/contract-testing/src/types.rs @@ -439,6 +439,8 @@ pub struct ContractInvokeSuccess { pub trace_elements: Vec, /// Energy used. pub energy_used: Energy, + pub storage_energy: Energy, + pub module_load_energy: Energy, /// Cost of transaction. pub transaction_fee: Amount, /// The returned value. @@ -1020,6 +1022,8 @@ pub enum InvokeExecutionError { pub struct ContractInvokeError { /// The energy used. pub energy_used: Energy, + /// Energy incurred by loading modules. + pub module_load_energy: Energy, /// The transaction fee. For [`Chain::contract_update`], this is the amount /// charged to the `invoker` account. pub transaction_fee: Amount, From 45004567e08a2e3d54db90c2e474a0f1c6183c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Mon, 15 Apr 2024 10:43:57 +0200 Subject: [PATCH 3/6] Change costs to P7 costs in the testing library. --- concordium-rust-sdk | 2 +- contract-testing/src/impls.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/concordium-rust-sdk b/concordium-rust-sdk index 06bf23bf..c8036f74 160000 --- a/concordium-rust-sdk +++ b/concordium-rust-sdk @@ -1 +1 @@ -Subproject commit 06bf23bf12ad281c1f66913a1b7db066e3827a96 +Subproject commit c8036f74c430cf7432e8c537e7b359e3ab0cc015 diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index a16643d1..d41b2800 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -585,7 +585,7 @@ impl Chain { sender_account.balance.total -= transaction_fee; // Construct the artifact. - let artifact = match wasm::utils::instantiate_with_metering::( + let artifact = match wasm::utils::instantiate_with_metering::( ValidationConfig::V1, CostConfigurationV1, &v1::ConcordiumAllowedImports { @@ -2155,7 +2155,7 @@ pub(crate) fn from_interpreter_energy(interpreter_energy: &InterpreterEnergy) -> /// Calculate the energy for looking up a [`ContractModule`]. pub(crate) fn lookup_module_cost(module: &ContractModule) -> Energy { // The ratio is from Concordium/Cost.hs::lookupModule - Energy::from(module.size / 50) + Energy::from(module.size / 500) } /// Calculate the microCCD(mCCD) cost of energy(NRG) using the two exchange From da523dd40da1972533a6fd82765fa840461bb176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Mon, 15 Apr 2024 10:55:06 +0200 Subject: [PATCH 4/6] Changelog. --- contract-testing/CHANGELOG.md | 4 ++++ contract-testing/src/impls.rs | 3 +++ 2 files changed, 7 insertions(+) diff --git a/contract-testing/CHANGELOG.md b/contract-testing/CHANGELOG.md index f3a719fa..84e022fa 100644 --- a/contract-testing/CHANGELOG.md +++ b/contract-testing/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased changes +- Integrate protocol version 7 cost semantics. +- The `ContractInvokeSuccess` and `ContractInvokeError` have additional fields + that record where parts of the energy was allocated during execution. + ## 4.2.0 - Add support for querying the module reference and contract name of an instance. diff --git a/contract-testing/src/impls.rs b/contract-testing/src/impls.rs index d41b2800..827c6d80 100644 --- a/contract-testing/src/impls.rs +++ b/contract-testing/src/impls.rs @@ -998,6 +998,9 @@ impl Chain { } } + // Since this is an internal function it seems better to allow rather than + // introduce a new struct just to call this function. + #[allow(clippy::too_many_arguments)] fn contract_invocation_process_response( &self, result: InvokeResponse, From c5d3a11cae6ed3f421506959af534b7d85d8d441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Mon, 15 Apr 2024 11:28:28 +0200 Subject: [PATCH 5/6] Fix tests for new costs. --- contract-testing/tests/basics.rs | 10 +++++----- contract-testing/tests/contract_queries.rs | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/contract-testing/tests/basics.rs b/contract-testing/tests/basics.rs index a4c6b66a..eddc0168 100644 --- a/contract-testing/tests/basics.rs +++ b/contract-testing/tests/basics.rs @@ -89,12 +89,12 @@ fn basics() { // Check that the energy usage matches the node. assert_eq!(deployment.energy_used, 1067.into()); - assert_eq!(init.energy_used, 771.into()); - assert_eq!(update.energy_used, 8198.into()); - assert_eq!(view.energy_used, 316.into()); + assert_eq!(init.energy_used, 756.into()); + assert_eq!(update.energy_used, 7787.into()); + assert_eq!(view.energy_used, 301.into()); // Check that the amounts charged matches the node. assert_eq!(deployment.transaction_fee, Amount::from_micro_ccd(2_685_078)); - assert_eq!(init.transaction_fee, Amount::from_micro_ccd(1_940_202)); - assert_eq!(update.transaction_fee, Amount::from_micro_ccd(20_630_050)); + assert_eq!(init.transaction_fee, Amount::from_micro_ccd(1_902_454)); + assert_eq!(update.transaction_fee, Amount::from_micro_ccd(19_595_780)); } diff --git a/contract-testing/tests/contract_queries.rs b/contract-testing/tests/contract_queries.rs index 541fdc64..416241f5 100644 --- a/contract-testing/tests/contract_queries.rs +++ b/contract-testing/tests/contract_queries.rs @@ -117,35 +117,35 @@ fn test_module_ref_and_name() { res.0, res_deploy_contract_inspection.module_reference, "Contract 0 should match contract inspection contract" ); - assert_eq!(res.1, 775.into(), "Expected energy to get module reference '{}'", res.0); + assert_eq!(res.1, 766.into(), "Expected energy to get module reference '{}'", res.0); let res = get_module_ref(&mut chain, res_init_1.contract_address) .expect("Contract 1 should be valid"); assert_eq!( res.0, res_deploy_account_balance.module_reference, "Contract 1 should match contract inspection contract" ); - assert_eq!(res.1, 775.into(), "Expected energy to get module reference '{}'", res.0); + assert_eq!(res.1, 766.into(), "Expected energy to get module reference '{}'", res.0); let res = get_module_ref(&mut chain, res_init_2.contract_address) .expect("Contract 2 should be valid"); assert_eq!( res.0, res_deploy_contract_inspection.module_reference, "Contract 2 should match contract inspection contract" ); - assert_eq!(res.1, 775.into(), "Expected energy to get module reference '{}'", res.0); + assert_eq!(res.1, 766.into(), "Expected energy to get module reference '{}'", res.0); let err = get_module_ref(&mut chain, ContractAddress { index: 3, subindex: 0, }) .expect_err("Contract <3,0> should be invalid"); assert_eq!(err.reject_code(), Some(-1), "Rejection code for get_module_ref on <3,0>"); - assert_eq!(err.energy_used, 743.into(), "Expected energy for failed get module ref."); + assert_eq!(err.energy_used, 734.into(), "Expected energy for failed get module ref."); let err = get_module_ref(&mut chain, ContractAddress { index: 1, subindex: 1, }) .expect_err("Contract <0,1> should be invalid"); assert_eq!(err.reject_code(), Some(-1), "Rejection code for get_module_ref on <0,1>"); - assert_eq!(err.energy_used, 743.into(), "Expected energy for failed get module ref."); + assert_eq!(err.energy_used, 734.into(), "Expected energy for failed get module ref."); // Use contract 2 to check the contract name of the contracts. let get_name = |chain: &mut Chain, @@ -183,7 +183,7 @@ fn test_module_ref_and_name() { ); assert_eq!( res.1, - 754.into(), + 745.into(), "Expected energy for get_contract_name on {}", res.0.as_contract_name() ); @@ -196,7 +196,7 @@ fn test_module_ref_and_name() { ); assert_eq!( res.1, - 754.into(), + 745.into(), "Expected energy for get_contract_name on {}", res.0.as_contract_name() ); @@ -209,7 +209,7 @@ fn test_module_ref_and_name() { ); assert_eq!( res.1, - 755.into(), + 746.into(), "Expected energy for get_contract_name on {}", res.0.as_contract_name() ); @@ -219,14 +219,14 @@ fn test_module_ref_and_name() { }) .expect_err("Contract <3,0> should be invalid"); assert_eq!(err.reject_code(), Some(-1), "Rejection code for get_module_ref on <3,0>"); - assert_eq!(err.energy_used, 741.into(), "Expected energy for failed get_contract_name"); + assert_eq!(err.energy_used, 732.into(), "Expected energy for failed get_contract_name"); let err = get_name(&mut chain, ContractAddress { index: 1, subindex: 1, }) .expect_err("Contract <0,1> should be invalid"); assert_eq!(err.reject_code(), Some(-1), "Rejection code for get_module_ref on <0,1>"); - assert_eq!(err.energy_used, 741.into(), "Expected energy for failed get_contract_name"); + assert_eq!(err.energy_used, 732.into(), "Expected energy for failed get_contract_name"); } #[test] From dbda4f27a2ee97bfbea9dd61a8edd24721eabed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Bizjak?= Date: Sun, 21 Apr 2024 23:52:15 +0200 Subject: [PATCH 6/6] Bump SDK submodule. --- concordium-rust-sdk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concordium-rust-sdk b/concordium-rust-sdk index c8036f74..97ac9c3d 160000 --- a/concordium-rust-sdk +++ b/concordium-rust-sdk @@ -1 +1 @@ -Subproject commit c8036f74c430cf7432e8c537e7b359e3ab0cc015 +Subproject commit 97ac9c3dcc366354ce0d7ee955606056640d9942