1use crate::{TempoBlockEnv, TempoTxEnv, instructions};
2use alloy_evm::{Database, precompiles::PrecompilesMap};
3use alloy_primitives::{Address, U256};
4use revm::{
5 Context, Inspector,
6 context::{Cfg, CfgEnv, ContextError, Evm, FrameStack},
7 handler::{
8 EthFrame, EvmTr, FrameInitOrResult, FrameTr, ItemOrResult, instructions::EthInstructions,
9 },
10 inspector::InspectorEvmTr,
11 interpreter::{InitialAndFloorGas, interpreter::EthInterpreter},
12};
13use tempo_chainspec::hardfork::TempoHardfork;
14use tempo_precompiles::storage::StorageActions;
15
16pub type TempoContext<DB> = Context<TempoBlockEnv, TempoTxEnv, CfgEnv<TempoHardfork>, DB>;
18
19#[derive(Debug, derive_more::Deref, derive_more::DerefMut)]
21#[expect(clippy::type_complexity)]
22pub struct TempoEvm<DB: Database, I> {
23 #[deref]
25 #[deref_mut]
26 pub inner: Evm<
27 TempoContext<DB>,
28 I,
29 EthInstructions<EthInterpreter, TempoContext<DB>>,
30 PrecompilesMap,
31 EthFrame<EthInterpreter>,
32 >,
33 pub(crate) collected_fee: U256,
35 pub validator_fee: U256,
40 pub(crate) fee_token: Option<Address>,
42 pub(crate) key_expiry: Option<u64>,
45 pub skip_valid_after_check: bool,
50 pub skip_liquidity_check: bool,
55 pub(crate) actions: StorageActions,
57}
58
59impl<DB: Database, I> TempoEvm<DB, I> {
60 pub fn new(ctx: TempoContext<DB>, inspector: I) -> Self {
62 Self::new_with_actions(ctx, inspector, StorageActions::disabled())
63 }
64
65 pub fn new_with_actions(ctx: TempoContext<DB>, inspector: I, actions: StorageActions) -> Self {
67 let precompiles =
68 tempo_precompiles::tempo_precompiles_with_actions(&ctx.cfg, actions.clone());
69
70 Self::new_inner(
71 Evm {
72 instruction: instructions::tempo_instructions(ctx.cfg.spec),
73 ctx,
74 inspector,
75 precompiles,
76 frame_stack: FrameStack::new(),
77 },
78 actions,
79 )
80 }
81
82 #[inline]
84 #[expect(clippy::type_complexity)]
85 fn new_inner(
86 inner: Evm<
87 TempoContext<DB>,
88 I,
89 EthInstructions<EthInterpreter, TempoContext<DB>>,
90 PrecompilesMap,
91 EthFrame<EthInterpreter>,
92 >,
93 actions: StorageActions,
94 ) -> Self {
95 Self {
96 inner,
97 collected_fee: U256::ZERO,
98 validator_fee: U256::ZERO,
99 fee_token: None,
100 key_expiry: None,
101 skip_valid_after_check: false,
102 skip_liquidity_check: false,
103 actions,
104 }
105 }
106
107 pub(crate) fn initial_gas_and_reservoir(
109 &self,
110 init_and_floor_gas: &InitialAndFloorGas,
111 ) -> (u64, u64) {
112 if !self.cfg.spec.is_t0() && init_and_floor_gas.initial_total_gas() > self.tx.gas_limit {
116 (u64::MAX, 0)
117 } else {
118 init_and_floor_gas
119 .initial_gas_and_reservoir(self.tx.gas_limit, self.cfg.tx_gas_limit_cap())
120 }
121 }
122}
123
124impl<DB: Database, I> TempoEvm<DB, I> {
125 pub fn with_inspector<OINSP>(self, inspector: OINSP) -> TempoEvm<DB, OINSP> {
127 let Self { inner, actions, .. } = self;
128 TempoEvm::new_inner(inner.with_inspector(inspector), actions)
129 }
130
131 pub fn with_precompiles(self, precompiles: PrecompilesMap) -> Self {
133 let Self { inner, actions, .. } = self;
134 Self::new_inner(inner.with_precompiles(precompiles), actions)
135 }
136
137 pub fn into_inspector(self) -> I {
139 self.inner.into_inspector()
140 }
141
142 pub fn clear(&mut self) {
144 self.collected_fee = U256::ZERO;
145 self.fee_token = None;
146 self.key_expiry = None;
147 }
148}
149
150impl<DB, I> EvmTr for TempoEvm<DB, I>
151where
152 DB: Database,
153{
154 type Context = TempoContext<DB>;
155 type Instructions = EthInstructions<EthInterpreter, TempoContext<DB>>;
156 type Precompiles = PrecompilesMap;
157 type Frame = EthFrame<EthInterpreter>;
158
159 fn all(
160 &self,
161 ) -> (
162 &Self::Context,
163 &Self::Instructions,
164 &Self::Precompiles,
165 &FrameStack<Self::Frame>,
166 ) {
167 self.inner.all()
168 }
169
170 fn all_mut(
171 &mut self,
172 ) -> (
173 &mut Self::Context,
174 &mut Self::Instructions,
175 &mut Self::Precompiles,
176 &mut FrameStack<Self::Frame>,
177 ) {
178 self.inner.all_mut()
179 }
180
181 fn frame_stack(&mut self) -> &mut FrameStack<Self::Frame> {
182 &mut self.inner.frame_stack
183 }
184
185 fn frame_init(
186 &mut self,
187 frame_input: <Self::Frame as FrameTr>::FrameInit,
188 ) -> Result<
189 ItemOrResult<&mut Self::Frame, <Self::Frame as FrameTr>::FrameResult>,
190 ContextError<DB::Error>,
191 > {
192 self.inner.frame_init(frame_input)
193 }
194
195 fn frame_run(&mut self) -> Result<FrameInitOrResult<Self::Frame>, ContextError<DB::Error>> {
196 self.inner.frame_run()
197 }
198
199 fn frame_return_result(
200 &mut self,
201 result: <Self::Frame as FrameTr>::FrameResult,
202 ) -> Result<Option<<Self::Frame as FrameTr>::FrameResult>, ContextError<DB::Error>> {
203 self.inner.frame_return_result(result)
204 }
205}
206
207impl<DB, I> InspectorEvmTr for TempoEvm<DB, I>
208where
209 DB: Database,
210 I: Inspector<TempoContext<DB>>,
211{
212 type Inspector = I;
213
214 fn all_inspector(
215 &self,
216 ) -> (
217 &Self::Context,
218 &Self::Instructions,
219 &Self::Precompiles,
220 &FrameStack<Self::Frame>,
221 &Self::Inspector,
222 ) {
223 self.inner.all_inspector()
224 }
225
226 fn all_mut_inspector(
227 &mut self,
228 ) -> (
229 &mut Self::Context,
230 &mut Self::Instructions,
231 &mut Self::Precompiles,
232 &mut FrameStack<Self::Frame>,
233 &mut Self::Inspector,
234 ) {
235 self.inner.all_mut_inspector()
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use crate::gas_params::{tempo_gas_params, tempo_gas_params_with_amsterdam};
242 use alloy_eips::eip7702::Authorization;
243 use alloy_evm::FromRecoveredTx;
244 use alloy_primitives::{Address, Bytes, TxKind, U256, bytes, hex};
245 use alloy_sol_types::{SolCall, SolError};
246 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
247 use p256::{
248 ecdsa::{SigningKey, signature::hazmat::PrehashSigner},
249 elliptic_curve::rand_core::OsRng,
250 };
251 use reth_evm::EvmInternals;
252 use revm::{
253 Context, DatabaseRef, ExecuteCommitEvm, ExecuteEvm, InspectEvm, MainContext,
254 bytecode::opcode,
255 context::{
256 CfgEnv, ContextTr, TxEnv,
257 result::{ExecutionResult, HaltReason},
258 },
259 database::{CacheDB, EmptyDB},
260 handler::system_call::SystemCallEvm,
261 inspector::{CountInspector, InspectSystemCallEvm},
262 state::{AccountInfo, Bytecode},
263 };
264 use sha2::{Digest, Sha256};
265 use tempo_chainspec::{constants::gas::STORAGE_CREDIT_VALUE, hardfork::TempoHardfork};
266 use tempo_contracts::precompiles::IStorageCredits::{self, Mode};
267 use tempo_precompiles::{
268 AuthorizedKey, DelegateCallNotAllowed, NONCE_PRECOMPILE_ADDRESS, PATH_USD_ADDRESS,
269 STORAGE_CREDITS_ADDRESS,
270 nonce::NonceManager,
271 storage::{FromWord, Handler, StorageCtx, evm::EvmPrecompileStorageProvider},
272 storage_credits::{CreditMode, StorageCredits},
273 test_util::TIP20Setup,
274 tip20::{ITIP20, TIP20Token},
275 };
276 use tempo_primitives::{
277 TempoTransaction,
278 transaction::{
279 KeyAuthorization, KeychainSignature, SignatureType, TempoSignedAuthorization,
280 tempo_transaction::Call,
281 tt_signature::{
282 PrimitiveSignature, TempoSignature, WebAuthnSignature, derive_p256_address,
283 normalize_p256_s,
284 },
285 },
286 };
287
288 use crate::{TempoBlockEnv, TempoEvm, TempoHaltReason, TempoInvalidTransaction, TempoTxEnv};
289 use revm::context::result::InvalidTransaction;
290
291 const DEFAULT_BALANCE: u128 = 1_000_000_000_000_000_000;
295
296 const IDENTITY_PRECOMPILE: Address = Address::new([
298 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x04,
299 ]);
300
301 fn create_evm() -> TempoEvm<CacheDB<EmptyDB>, ()> {
305 let db = CacheDB::new(EmptyDB::new());
306 let ctx = Context::mainnet()
307 .with_db(db)
308 .with_block(Default::default())
309 .with_cfg(Default::default())
310 .with_tx(Default::default());
311 TempoEvm::new(ctx, ())
312 }
313
314 fn create_evm_with_timestamp(timestamp: u64) -> TempoEvm<CacheDB<EmptyDB>, ()> {
316 let db = CacheDB::new(EmptyDB::new());
317 let mut block = TempoBlockEnv::default();
318 block.inner.timestamp = U256::from(timestamp);
319
320 let ctx = Context::mainnet()
321 .with_db(db)
322 .with_block(block)
323 .with_cfg(Default::default())
324 .with_tx(Default::default());
325
326 TempoEvm::new(ctx, ())
327 }
328
329 fn fund_account(evm: &mut TempoEvm<CacheDB<EmptyDB>, ()>, address: Address) {
331 fund_account_with_nonce(evm, address, 0);
332 }
333
334 fn fund_account_with_nonce(
336 evm: &mut TempoEvm<CacheDB<EmptyDB>, ()>,
337 address: Address,
338 nonce: u64,
339 ) {
340 evm.ctx.db_mut().insert_account_info(
341 address,
342 AccountInfo {
343 balance: U256::from(DEFAULT_BALANCE),
344 nonce,
345 ..Default::default()
346 },
347 );
348 }
349
350 fn create_funded_evm(address: Address) -> TempoEvm<CacheDB<EmptyDB>, ()> {
352 let mut evm = create_evm();
353 fund_account(&mut evm, address);
354 evm
355 }
356
357 fn create_funded_evm_t1(address: Address) -> TempoEvm<CacheDB<EmptyDB>, ()> {
360 let db = CacheDB::new(EmptyDB::new());
361 let mut cfg = CfgEnv::<TempoHardfork>::default();
362 cfg.spec = TempoHardfork::T1C;
363 cfg.gas_params = tempo_gas_params(TempoHardfork::T1C);
365
366 let ctx = Context::mainnet()
367 .with_db(db)
368 .with_block(Default::default())
369 .with_cfg(cfg)
370 .with_tx(Default::default());
371
372 let mut evm = TempoEvm::new(ctx, ());
373 fund_account(&mut evm, address);
374 evm
375 }
376
377 fn create_funded_evm_t3(address: Address) -> TempoEvm<CacheDB<EmptyDB>, ()> {
379 let db = CacheDB::new(EmptyDB::new());
380 let mut cfg = CfgEnv::<TempoHardfork>::default();
381 cfg.spec = TempoHardfork::T3;
382 cfg.gas_params = tempo_gas_params(TempoHardfork::T3);
383
384 let ctx = Context::mainnet()
385 .with_db(db)
386 .with_block(Default::default())
387 .with_cfg(cfg)
388 .with_tx(Default::default());
389
390 let mut evm = TempoEvm::new(ctx, ());
391 fund_account(&mut evm, address);
392 evm
393 }
394
395 fn create_funded_evm_t4(address: Address) -> TempoEvm<CacheDB<EmptyDB>, ()> {
397 let db = CacheDB::new(EmptyDB::new());
398 let mut cfg = CfgEnv::<TempoHardfork>::default();
399 cfg.spec = TempoHardfork::T4;
400 cfg.gas_params = tempo_gas_params(TempoHardfork::T4);
401 cfg.enable_amsterdam_eip8037 = true;
402
403 let ctx = Context::mainnet()
404 .with_db(db)
405 .with_block(Default::default())
406 .with_cfg(cfg)
407 .with_tx(Default::default());
408
409 let mut evm = TempoEvm::new(ctx, ());
410 fund_account(&mut evm, address);
411 evm
412 }
413
414 fn create_funded_evm_t7(address: Address) -> TempoEvm<CacheDB<EmptyDB>, ()> {
418 let db = CacheDB::new(EmptyDB::new());
419 let mut cfg = CfgEnv::<TempoHardfork>::default();
420 cfg.spec = TempoHardfork::T7;
421 cfg.gas_params = tempo_gas_params_with_amsterdam(TempoHardfork::T7, false);
422 cfg.enable_amsterdam_eip8037 = false;
423
424 let ctx = Context::mainnet()
425 .with_db(db)
426 .with_block(Default::default())
427 .with_cfg(cfg)
428 .with_tx(Default::default());
429
430 let mut evm = TempoEvm::new(ctx, ());
431 fund_account(&mut evm, address);
432 evm
433 }
434
435 fn create_funded_evm_t7_with_timestamp(
437 address: Address,
438 timestamp: u64,
439 ) -> TempoEvm<CacheDB<EmptyDB>, ()> {
440 let db = CacheDB::new(EmptyDB::new());
441 let mut cfg = CfgEnv::<TempoHardfork>::default();
442 cfg.spec = TempoHardfork::T7;
443 cfg.gas_params = tempo_gas_params_with_amsterdam(TempoHardfork::T7, false);
444 cfg.enable_amsterdam_eip8037 = false;
445
446 let mut block = TempoBlockEnv::default();
447 block.inner.timestamp = U256::from(timestamp);
448
449 let ctx = Context::mainnet()
450 .with_db(db)
451 .with_block(block)
452 .with_cfg(cfg)
453 .with_tx(Default::default());
454
455 let mut evm = TempoEvm::new(ctx, ());
456 fund_account(&mut evm, address);
457 evm
458 }
459
460 fn create_funded_evm_with_timestamp(
462 address: Address,
463 timestamp: u64,
464 ) -> TempoEvm<CacheDB<EmptyDB>, ()> {
465 let mut evm = create_evm_with_timestamp(timestamp);
466 fund_account(&mut evm, address);
467 evm
468 }
469
470 fn create_funded_evm_t1_with_timestamp(
472 address: Address,
473 timestamp: u64,
474 ) -> TempoEvm<CacheDB<EmptyDB>, ()> {
475 let db = CacheDB::new(EmptyDB::new());
476 let mut cfg = CfgEnv::<TempoHardfork>::default();
477 cfg.spec = TempoHardfork::T1;
478 cfg.gas_params = tempo_gas_params(TempoHardfork::T1);
479
480 let mut block = TempoBlockEnv::default();
481 block.inner.timestamp = U256::from(timestamp);
482
483 let ctx = Context::mainnet()
484 .with_db(db)
485 .with_block(block)
486 .with_cfg(cfg)
487 .with_tx(Default::default());
488
489 let mut evm = TempoEvm::new(ctx, ());
490 fund_account(&mut evm, address);
491 evm
492 }
493
494 fn create_evm_with_inspector<I>(inspector: I) -> TempoEvm<CacheDB<EmptyDB>, I> {
496 let db = CacheDB::new(EmptyDB::new());
497 let ctx = Context::mainnet()
498 .with_db(db)
499 .with_block(Default::default())
500 .with_cfg(Default::default())
501 .with_tx(Default::default());
502 TempoEvm::new(ctx, inspector)
503 }
504
505 struct P256KeyPair {
507 signing_key: SigningKey,
508 pub_key_x: alloy_primitives::B256,
509 pub_key_y: alloy_primitives::B256,
510 address: Address,
511 }
512
513 impl P256KeyPair {
514 fn random() -> Self {
516 let signing_key = SigningKey::random(&mut OsRng);
517 let verifying_key = signing_key.verifying_key();
518 let encoded_point = verifying_key.to_encoded_point(false);
519 let pub_key_x = alloy_primitives::B256::from_slice(encoded_point.x().unwrap().as_ref());
520 let pub_key_y = alloy_primitives::B256::from_slice(encoded_point.y().unwrap().as_ref());
521 let address = derive_p256_address(&pub_key_x, &pub_key_y);
522
523 Self {
524 signing_key,
525 pub_key_x,
526 pub_key_y,
527 address,
528 }
529 }
530
531 fn sign_webauthn(&self, challenge: &[u8]) -> eyre::Result<WebAuthnSignature> {
533 let mut authenticator_data = vec![0u8; 37];
535 authenticator_data[0..32].copy_from_slice(&[0xAA; 32]); authenticator_data[32] = 0x01; authenticator_data[33..37].copy_from_slice(&[0, 0, 0, 0]); let challenge_b64url = URL_SAFE_NO_PAD.encode(challenge);
541 let client_data_json = format!(
542 r#"{{"type":"webauthn.get","challenge":"{challenge_b64url}","origin":"https://example.com","crossOrigin":false}}"#
543 );
544
545 let client_data_hash = Sha256::digest(client_data_json.as_bytes());
547 let mut final_hasher = Sha256::new();
548 final_hasher.update(&authenticator_data);
549 final_hasher.update(client_data_hash);
550 let message_hash = final_hasher.finalize();
551
552 let signature: p256::ecdsa::Signature = self.signing_key.sign_prehash(&message_hash)?;
554 let sig_bytes = signature.to_bytes();
555
556 let mut webauthn_data = Vec::new();
558 webauthn_data.extend_from_slice(&authenticator_data);
559 webauthn_data.extend_from_slice(client_data_json.as_bytes());
560
561 Ok(WebAuthnSignature {
562 webauthn_data: Bytes::from(webauthn_data),
563 r: alloy_primitives::B256::from_slice(&sig_bytes[0..32]),
564 s: normalize_p256_s(&sig_bytes[32..64]).map_err(|e| eyre::eyre!(e))?,
565 pub_key_x: self.pub_key_x,
566 pub_key_y: self.pub_key_y,
567 })
568 }
569
570 fn create_signed_authorization(
572 &self,
573 delegate_address: Address,
574 ) -> eyre::Result<TempoSignedAuthorization> {
575 let auth = Authorization {
576 chain_id: U256::from(1),
577 address: delegate_address,
578 nonce: 0,
579 };
580
581 let mut sig_buf = Vec::new();
582 sig_buf.push(tempo_primitives::transaction::tt_authorization::MAGIC);
583 alloy_rlp::Encodable::encode(&auth, &mut sig_buf);
584 let auth_sig_hash = alloy_primitives::keccak256(&sig_buf);
585
586 let webauthn_sig = self.sign_webauthn(auth_sig_hash.as_slice())?;
587 let aa_sig = TempoSignature::Primitive(PrimitiveSignature::WebAuthn(webauthn_sig));
588
589 Ok(TempoSignedAuthorization::new_unchecked(auth, aa_sig))
590 }
591
592 fn sign_tx(&self, tx: TempoTransaction) -> eyre::Result<tempo_primitives::AASigned> {
594 let webauthn_sig = self.sign_webauthn(tx.signature_hash().as_slice())?;
595 Ok(
596 tx.into_signed(TempoSignature::Primitive(PrimitiveSignature::WebAuthn(
597 webauthn_sig,
598 ))),
599 )
600 }
601
602 fn sign_tx_keychain(
603 &self,
604 tx: TempoTransaction,
605 ) -> eyre::Result<tempo_primitives::AASigned> {
606 let sig_hash = tx.signature_hash();
608 let effective_hash = alloy_primitives::keccak256(
609 [&[0x04], sig_hash.as_slice(), self.address.as_slice()].concat(),
610 );
611 let webauthn_sig = self.sign_webauthn(effective_hash.as_slice())?;
612 let keychain_sig =
613 KeychainSignature::new(self.address, PrimitiveSignature::WebAuthn(webauthn_sig));
614 Ok(tx.into_signed(TempoSignature::Keychain(keychain_sig)))
615 }
616 }
617
618 struct TxBuilder {
620 calls: Vec<Call>,
621 nonce: u64,
622 nonce_key: U256,
623 gas_limit: u64,
624 max_fee_per_gas: u128,
625 max_priority_fee_per_gas: u128,
626 valid_before: Option<u64>,
627 valid_after: Option<u64>,
628 authorization_list: Vec<TempoSignedAuthorization>,
629 key_authorization: Option<tempo_primitives::transaction::SignedKeyAuthorization>,
630 }
631
632 impl Default for TxBuilder {
633 fn default() -> Self {
634 Self {
635 calls: vec![],
636 nonce: 0,
637 nonce_key: U256::ZERO,
638 gas_limit: 1_000_000,
639 max_fee_per_gas: 0,
640 max_priority_fee_per_gas: 0,
641 valid_before: Some(u64::MAX),
642 valid_after: None,
643 authorization_list: vec![],
644 key_authorization: None,
645 }
646 }
647 }
648
649 impl TxBuilder {
650 fn new() -> Self {
651 Self::default()
652 }
653
654 fn call_identity(mut self, input: &[u8]) -> Self {
656 self.calls.push(Call {
657 to: TxKind::Call(IDENTITY_PRECOMPILE),
658 value: U256::ZERO,
659 input: Bytes::from(input.to_vec()),
660 });
661 self
662 }
663
664 fn call(mut self, to: Address, input: &[u8]) -> Self {
666 self.calls.push(Call {
667 to: TxKind::Call(to),
668 value: U256::ZERO,
669 input: Bytes::from(input.to_vec()),
670 });
671 self
672 }
673
674 fn create(mut self, initcode: &[u8]) -> Self {
676 self.calls.push(Call {
677 to: TxKind::Create,
678 value: U256::ZERO,
679 input: Bytes::from(initcode.to_vec()),
680 });
681 self
682 }
683
684 fn call_with_value(mut self, to: Address, input: &[u8], value: U256) -> Self {
686 self.calls.push(Call {
687 to: TxKind::Call(to),
688 value,
689 input: Bytes::from(input.to_vec()),
690 });
691 self
692 }
693
694 fn nonce(mut self, nonce: u64) -> Self {
695 self.nonce = nonce;
696 self
697 }
698
699 fn nonce_key(mut self, nonce_key: U256) -> Self {
700 self.nonce_key = nonce_key;
701 self
702 }
703
704 fn gas_limit(mut self, gas_limit: u64) -> Self {
705 self.gas_limit = gas_limit;
706 self
707 }
708
709 fn with_max_fee_per_gas(mut self, max_fee_per_gas: u128) -> Self {
710 self.max_fee_per_gas = max_fee_per_gas;
711 self
712 }
713
714 fn with_max_priority_fee_per_gas(mut self, max_priority_fee_per_gas: u128) -> Self {
715 self.max_priority_fee_per_gas = max_priority_fee_per_gas;
716 self
717 }
718
719 fn valid_before(mut self, valid_before: Option<u64>) -> Self {
720 self.valid_before = valid_before;
721 self
722 }
723
724 fn valid_after(mut self, valid_after: Option<u64>) -> Self {
725 self.valid_after = valid_after;
726 self
727 }
728
729 fn authorization(mut self, auth: TempoSignedAuthorization) -> Self {
730 self.authorization_list.push(auth);
731 self
732 }
733
734 fn key_authorization(
735 mut self,
736 key_auth: tempo_primitives::transaction::SignedKeyAuthorization,
737 ) -> Self {
738 self.key_authorization = Some(key_auth);
739 self
740 }
741
742 fn build(self) -> TempoTransaction {
743 TempoTransaction {
744 chain_id: 1,
745 fee_token: None,
746 max_priority_fee_per_gas: self.max_priority_fee_per_gas,
747 max_fee_per_gas: self.max_fee_per_gas,
748 gas_limit: self.gas_limit,
749 calls: self.calls,
750 access_list: Default::default(),
751 nonce_key: self.nonce_key,
752 nonce: self.nonce,
753 fee_payer_signature: None,
754 valid_before: self.valid_before.and_then(core::num::NonZeroU64::new),
755 valid_after: self.valid_after.and_then(core::num::NonZeroU64::new),
756 key_authorization: self.key_authorization,
757 tempo_authorization_list: self.authorization_list,
758 }
759 }
760 }
761
762 #[test_case::test_case(TempoHardfork::T1)]
765 #[test_case::test_case(TempoHardfork::T1C)]
766 fn test_access_millis_timestamp(spec: TempoHardfork) -> eyre::Result<()> {
767 let db = CacheDB::new(EmptyDB::new());
768
769 let mut ctx = Context::mainnet()
770 .with_db(db)
771 .with_block(TempoBlockEnv::default())
772 .with_cfg(CfgEnv::<TempoHardfork>::default())
773 .with_tx(Default::default());
774
775 ctx.cfg.spec = spec;
776 ctx.block.timestamp = U256::from(1000);
777 ctx.block.timestamp_millis_part = 100;
778
779 let mut tempo_evm = TempoEvm::new(ctx, ());
780 let ctx = &mut tempo_evm.ctx;
781
782 let internals = EvmInternals::new(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, &ctx.tx);
783 let mut storage = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
784
785 _ = StorageCtx::enter(&mut storage, || TIP20Setup::path_usd(Address::ZERO).apply())?;
786 drop(storage);
787
788 let contract = Address::random();
789
790 ctx.db_mut().insert_account_info(
792 contract,
793 AccountInfo {
794 code: Some(Bytecode::new_raw(bytes!("0x4F5F5260205FF3"))),
796 ..Default::default()
797 },
798 );
799
800 let tx_env = TxEnv {
801 kind: contract.into(),
802 ..Default::default()
803 };
804 let result = tempo_evm.transact_one(tx_env.into())?;
805
806 if !spec.is_t1c() {
807 assert!(result.is_success());
808 assert_eq!(
809 U256::from_be_slice(result.output().unwrap()),
810 U256::from(1000100)
811 );
812 } else {
813 assert!(matches!(
814 result,
815 ExecutionResult::Halt {
816 reason: TempoHaltReason::Ethereum(HaltReason::OpcodeNotFound),
817 ..
818 }
819 ));
820 }
821
822 Ok(())
823 }
824
825 #[test]
826 fn test_inspector_calls() -> eyre::Result<()> {
827 let caller = Address::repeat_byte(0x01);
829 let contract = Address::repeat_byte(0x42);
830
831 let input_bytes = ITIP20::setSupplyCapCall {
832 newSupplyCap: U256::from(100),
833 }
834 .abi_encode();
835
836 let mut bytecode_bytes = vec![];
839
840 for (i, &byte) in input_bytes.iter().enumerate() {
841 bytecode_bytes.extend_from_slice(&[
842 opcode::PUSH1,
843 byte,
844 opcode::PUSH1,
845 i as u8,
846 opcode::MSTORE8,
847 ]);
848 }
849
850 bytecode_bytes.extend_from_slice(&[
853 opcode::PUSH1,
854 0x00, opcode::PUSH1,
856 0x00, opcode::PUSH1,
858 0x24, opcode::PUSH1,
860 0x00, opcode::PUSH1,
862 0x00, ]);
864
865 bytecode_bytes.push(opcode::PUSH20);
867 bytecode_bytes.extend_from_slice(PATH_USD_ADDRESS.as_slice());
868
869 bytecode_bytes.extend_from_slice(&[
870 opcode::PUSH2,
871 0xFF,
872 0xFF, opcode::CALL,
874 opcode::POP, opcode::STOP,
876 ]);
877
878 let bytecode = Bytecode::new_raw(bytecode_bytes.into());
879
880 let mut evm = create_evm_with_inspector(CountInspector::new());
882 {
884 let ctx = &mut evm.ctx;
885 let internals =
886 EvmInternals::new(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, &ctx.tx);
887
888 let mut storage = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
889 StorageCtx::enter(&mut storage, || {
890 TIP20Setup::path_usd(caller)
891 .with_issuer(caller)
892 .with_admin(contract) .apply()
894 })?;
895 }
896
897 evm.ctx.db_mut().insert_account_info(
899 contract,
900 AccountInfo {
901 code: Some(bytecode),
902 ..Default::default()
903 },
904 );
905
906 let tx_env = TxEnv {
908 caller,
909 kind: TxKind::Call(contract),
910 gas_limit: 1_000_000,
911 ..Default::default()
912 };
913 let result = evm
914 .inspect_tx(tx_env.into())
915 .expect("execution should succeed");
916
917 assert!(result.result.is_success());
918
919 assert_eq!(result.result.logs().len(), 3);
921 assert_eq!(result.result.logs()[0].address, PATH_USD_ADDRESS);
923
924 let inspector = &evm.inspector;
926
927 assert_eq!(inspector.get_count(opcode::CALL), 1);
929
930 assert_eq!(inspector.get_count(opcode::STOP), 1);
931
932 assert_eq!(inspector.log_count(), 1);
934
935 assert_eq!(inspector.call_count(), 2);
937
938 assert_eq!(inspector.call_end_count(), 2);
940
941 let key_pair = P256KeyPair::random();
945 let tempo_caller = key_pair.address;
946
947 let signed_auth = key_pair.create_signed_authorization(Address::repeat_byte(0x42))?;
949
950 let tx = TxBuilder::new()
952 .call_identity(&[0x01, 0x02])
953 .call_identity(&[0x03, 0x04])
954 .call_identity(&[0x05, 0x06])
955 .authorization(signed_auth)
956 .build();
957
958 let signed_tx = key_pair.sign_tx(tx)?;
959 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, tempo_caller);
960
961 let mut multi_evm = create_evm_with_inspector(CountInspector::new());
963 multi_evm.ctx.db_mut().insert_account_info(
964 tempo_caller,
965 AccountInfo {
966 balance: U256::from(DEFAULT_BALANCE),
967 ..Default::default()
968 },
969 );
970
971 let multi_result = multi_evm.inspect_tx(tx_env)?;
973 assert!(multi_result.result.is_success(),);
974
975 let multi_inspector = &multi_evm.inspector;
977
978 assert_eq!(multi_inspector.call_count(), 3,);
981 assert_eq!(multi_inspector.call_end_count(), 3,);
982
983 Ok(())
984 }
985
986 #[test]
987 fn test_tempo_tx_initial_gas() -> eyre::Result<()> {
988 let key_pair = P256KeyPair::random();
989 let caller = key_pair.address;
990
991 let mut evm = create_funded_evm(caller);
993 evm.block.basefee = 100_000_000_000;
994
995 let block = TempoBlockEnv::default();
997 let ctx = &mut evm.ctx;
998 let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
999 let mut provider =
1000 EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
1001
1002 StorageCtx::enter(&mut provider, || {
1003 TIP20Setup::path_usd(caller)
1004 .with_issuer(caller)
1005 .with_mint(caller, U256::from(100_000))
1006 .apply()
1007 })?;
1008
1009 drop(provider);
1010
1011 let tx1 = TxBuilder::new()
1013 .call_identity(&[])
1014 .gas_limit(300_000)
1015 .with_max_fee_per_gas(200_000_000_000)
1016 .with_max_priority_fee_per_gas(0)
1017 .build();
1018
1019 let signed_tx1 = key_pair.sign_tx(tx1)?;
1020 let tx_env1 = TempoTxEnv::from_recovered_tx(&signed_tx1, caller);
1021
1022 let ctx = &mut evm.ctx;
1023 let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
1024 let mut provider =
1025 EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
1026
1027 let slot = StorageCtx::enter(&mut provider, || {
1028 TIP20Token::from_address(PATH_USD_ADDRESS)?.balances[caller].read()
1029 })?;
1030 drop(provider);
1031
1032 assert_eq!(slot, U256::from(100_000));
1033
1034 let result1 = evm.transact_commit(tx_env1)?;
1035 assert!(result1.is_success());
1036 assert_eq!(result1.tx_gas_used(), 28_671);
1037
1038 let ctx = &mut evm.ctx;
1039 let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
1040 let mut provider =
1041 EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
1042
1043 let slot = StorageCtx::enter(&mut provider, || {
1044 TIP20Token::from_address(PATH_USD_ADDRESS)?.balances[caller].read()
1045 })?;
1046 drop(provider);
1047
1048 assert_eq!(slot, U256::from(97_132));
1049
1050 let tx2 = TxBuilder::new()
1052 .call_identity(&[])
1053 .call_identity(&[])
1054 .nonce(1)
1055 .gas_limit(35_000)
1056 .with_max_fee_per_gas(200_000_000_000)
1057 .with_max_priority_fee_per_gas(0)
1058 .build();
1059
1060 let signed_tx2 = key_pair.sign_tx(tx2)?;
1061 let tx_env2 = TempoTxEnv::from_recovered_tx(&signed_tx2, caller);
1062
1063 let result2 = evm.transact_commit(tx_env2)?;
1064 assert!(result2.is_success());
1065 assert_eq!(result2.tx_gas_used(), 31_286);
1066
1067 let ctx = &mut evm.ctx;
1068 let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
1069 let mut provider =
1070 EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
1071
1072 let slot = StorageCtx::enter(&mut provider, || {
1073 TIP20Token::from_address(PATH_USD_ADDRESS)?.balances[caller].read()
1074 })?;
1075 drop(provider);
1076
1077 assert_eq!(slot, U256::from(94_003));
1078
1079 Ok(())
1080 }
1081
1082 #[test]
1087 fn test_tempo_tx() -> eyre::Result<()> {
1088 let key_pair = P256KeyPair::random();
1089 let caller = key_pair.address;
1090
1091 let signed_auth = key_pair.create_signed_authorization(Address::repeat_byte(0x42))?;
1093
1094 let tx = TxBuilder::new()
1096 .call_identity(&[0x01, 0x02, 0x03, 0x04])
1097 .call_identity(&[0xAA, 0xBB, 0xCC, 0xDD])
1098 .authorization(signed_auth.clone())
1099 .build();
1100
1101 let signed_tx = key_pair.sign_tx(tx)?;
1102 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1103
1104 assert!(tx_env.tempo_tx_env.is_some(),);
1106 let tempo_env = tx_env.tempo_tx_env.as_ref().unwrap();
1107 assert_eq!(tempo_env.tempo_authorization_list.len(), 1);
1108 assert_eq!(tempo_env.aa_calls.len(), 2);
1109
1110 let mut evm = create_funded_evm_t1(caller);
1112
1113 let result = evm.transact_commit(tx_env)?;
1115 assert!(result.is_success());
1116
1117 let key_auth = KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, caller);
1119 let key_auth_webauthn_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
1120 let signed_key_auth =
1121 key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_webauthn_sig));
1122
1123 let tx2 = TxBuilder::new()
1125 .call_identity(&[0x01, 0x02, 0x03, 0x04])
1126 .call_identity(&[0xAA, 0xBB, 0xCC, 0xDD])
1127 .authorization(signed_auth)
1128 .nonce(1)
1129 .gas_limit(1_000_000)
1130 .key_authorization(signed_key_auth)
1131 .build();
1132
1133 let signed_tx = key_pair.sign_tx_keychain(tx2)?;
1134 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1135
1136 let tempo_env_keychain = tx_env
1138 .tempo_tx_env
1139 .as_ref()
1140 .expect("Transaction should have tempo_tx_env");
1141 let keychain_sig = tempo_env_keychain
1142 .signature
1143 .as_keychain()
1144 .expect("Signature should be a KeychainSignature");
1145
1146 assert_eq!(keychain_sig.user_address, caller,);
1149
1150 assert!(matches!(
1152 keychain_sig.signature,
1153 PrimitiveSignature::WebAuthn(_)
1154 ));
1155
1156 let recovered_key_id = keychain_sig
1158 .key_id(&tempo_env_keychain.signature_hash)
1159 .expect("Key ID recovery should succeed");
1160 assert_eq!(recovered_key_id, caller,);
1161
1162 let result = evm.transact_commit(tx_env)?;
1164 assert!(result.is_success());
1165
1166 let tx_fail = TxBuilder::new()
1168 .call(PATH_USD_ADDRESS, &[0x01, 0x02]) .nonce(2)
1170 .build();
1171
1172 let signed_tx_fail = key_pair.sign_tx_keychain(tx_fail)?;
1173 let tx_env_fail = TempoTxEnv::from_recovered_tx(&signed_tx_fail, caller);
1174
1175 let result_fail = evm.transact(tx_env_fail)?;
1176 assert!(!result_fail.result.is_success());
1177
1178 let nonce_key_2d = U256::from(42);
1180
1181 let tx_2d = TxBuilder::new()
1182 .call_identity(&[0x2D, 0x2D, 0x2D, 0x2D])
1183 .nonce_key(nonce_key_2d)
1184 .build();
1185
1186 let signed_tx_2d = key_pair.sign_tx_keychain(tx_2d)?;
1187 let tx_env_2d = TempoTxEnv::from_recovered_tx(&signed_tx_2d, caller);
1188
1189 assert!(tx_env_2d.tempo_tx_env.is_some());
1190 assert_eq!(
1191 tx_env_2d.tempo_tx_env.as_ref().unwrap().nonce_key,
1192 nonce_key_2d
1193 );
1194
1195 let result_2d = evm.transact_commit(tx_env_2d)?;
1196 assert!(result_2d.is_success());
1197
1198 let nonce_slot = NonceManager::new().nonces[caller][nonce_key_2d].slot();
1200 let stored_nonce = evm
1201 .ctx
1202 .db()
1203 .storage_ref(NONCE_PRECOMPILE_ADDRESS, nonce_slot)
1204 .unwrap_or_default();
1205 assert_eq!(stored_nonce, U256::from(1));
1206
1207 let tx_2d_2 = TxBuilder::new()
1209 .call_identity(&[0x2E, 0x2E, 0x2E, 0x2E])
1210 .nonce_key(nonce_key_2d)
1211 .nonce(1)
1212 .build();
1213
1214 let signed_tx_2d_2 = key_pair.sign_tx_keychain(tx_2d_2)?;
1215 let tx_env_2d_2 = TempoTxEnv::from_recovered_tx(&signed_tx_2d_2, caller);
1216
1217 let result_2d_2 = evm.transact_commit(tx_env_2d_2)?;
1218 assert!(result_2d_2.is_success());
1219
1220 let stored_nonce_2 = evm
1222 .ctx
1223 .db()
1224 .storage_ref(NONCE_PRECOMPILE_ADDRESS, nonce_slot)
1225 .unwrap_or_default();
1226 assert_eq!(stored_nonce_2, U256::from(2));
1227
1228 Ok(())
1229 }
1230
1231 #[test]
1232 fn test_t3_key_authorization_deny_all_scopes_blocks_same_tx_call() -> eyre::Result<()> {
1233 let key_pair = P256KeyPair::random();
1234 let caller = key_pair.address;
1235
1236 let mut evm = create_funded_evm_t3(caller);
1237
1238 let block = TempoBlockEnv::default();
1240 {
1241 let ctx = &mut evm.ctx;
1242 let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
1243 let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
1244
1245 StorageCtx::enter(&mut provider, || {
1246 TIP20Setup::path_usd(caller)
1247 .with_issuer(caller)
1248 .with_mint(caller, U256::from(10_000_000))
1249 .apply()
1250 })?;
1251 }
1252
1253 let key_auth =
1255 KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, caller).with_no_calls();
1256 let key_auth_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
1257 let signed_key_auth = key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig));
1258
1259 let tx = TxBuilder::new()
1260 .call_identity(&[0x01])
1261 .key_authorization(signed_key_auth)
1262 .gas_limit(5_000_000)
1263 .build();
1264
1265 let signed_tx = key_pair.sign_tx_keychain(tx)?;
1267 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1268
1269 let result = evm.transact_commit(tx_env)?;
1270 assert!(
1271 !result.is_success(),
1272 "deny-all scope should now fail during paid execution"
1273 );
1274 assert!(
1275 result.tx_gas_used() > 0,
1276 "failed execution should still consume gas"
1277 );
1278
1279 Ok(())
1280 }
1281
1282 #[test]
1283 fn test_t3_key_authorization_accepts_empty_recipient_allowlist_as_unconstrained()
1284 -> eyre::Result<()> {
1285 let key_pair = P256KeyPair::random();
1286 let caller = key_pair.address;
1287
1288 let mut evm = create_funded_evm_t3(caller);
1289
1290 let block = TempoBlockEnv::default();
1291 {
1292 let ctx = &mut evm.ctx;
1293 let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
1294 let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
1295
1296 StorageCtx::enter(&mut provider, || {
1297 TIP20Setup::path_usd(caller)
1298 .with_issuer(caller)
1299 .with_mint(caller, U256::from(10_000_000))
1300 .apply()
1301 })?;
1302 }
1303
1304 let transfer_to = Address::repeat_byte(0xaa);
1305 let transfer_input = ITIP20::transferCall {
1306 to: transfer_to,
1307 amount: U256::from(1_u64),
1308 }
1309 .abi_encode();
1310
1311 let key_auth = KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, caller)
1312 .with_allowed_calls(vec![tempo_primitives::transaction::CallScope {
1313 target: PATH_USD_ADDRESS,
1314 selector_rules: vec![tempo_primitives::transaction::SelectorRule {
1315 selector: ITIP20::transferCall::SELECTOR,
1316 recipients: Vec::new(),
1317 }],
1318 }]);
1319 let key_auth_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
1320 let signed_key_auth = key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig));
1321
1322 let tx = TxBuilder::new()
1323 .call(PATH_USD_ADDRESS, &transfer_input)
1324 .key_authorization(signed_key_auth)
1325 .gas_limit(5_000_000)
1326 .build();
1327
1328 let signed_tx = key_pair.sign_tx_keychain(tx)?;
1329 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1330
1331 evm.transact_commit(tx_env)
1332 .expect("empty recipient allowlist should allow the call");
1333
1334 Ok(())
1335 }
1336
1337 #[test]
1338 fn test_same_tx_key_authorization_rejects_key_type_mismatch() -> eyre::Result<()> {
1339 let key_pair = P256KeyPair::random();
1340 let caller = key_pair.address;
1341
1342 let mut evm = create_funded_evm_t3(caller);
1343
1344 let key_auth = KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, caller);
1345 let key_auth_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
1346 let signed_key_auth = key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig));
1347
1348 let tx = TxBuilder::new()
1349 .call_identity(&[0x01])
1350 .key_authorization(signed_key_auth)
1351 .gas_limit(5_000_000)
1352 .build();
1353
1354 let signed_tx = key_pair.sign_tx_keychain(tx)?;
1355 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1356
1357 let err = evm
1358 .transact_commit(tx_env)
1359 .expect_err("mismatched key_type should reject same-tx auth+use");
1360
1361 assert!(
1362 matches!(
1363 err,
1364 revm::context::result::EVMError::Transaction(
1365 TempoInvalidTransaction::KeychainValidationFailed { .. }
1366 )
1367 ),
1368 "expected KeychainValidationFailed, got: {err:?}"
1369 );
1370
1371 Ok(())
1372 }
1373
1374 #[test]
1377 fn test_tempo_tx_time_window() -> eyre::Result<()> {
1378 let key_pair = P256KeyPair::random();
1379 let caller = key_pair.address;
1380
1381 let signed_auth = key_pair.create_signed_authorization(Address::repeat_byte(0x42))?;
1383
1384 let create_signed_tx = |valid_after: Option<u64>, valid_before: Option<u64>| {
1386 let tx = TxBuilder::new()
1387 .call_identity(&[0x01, 0x02, 0x03, 0x04])
1388 .authorization(signed_auth.clone())
1389 .valid_after(valid_after)
1390 .valid_before(valid_before)
1391 .build();
1392 key_pair.sign_tx(tx)
1393 };
1394
1395 {
1397 let mut evm = create_funded_evm_with_timestamp(caller, 100);
1398 let signed_tx = create_signed_tx(Some(200), None)?;
1399 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1400
1401 let result = evm.transact(tx_env);
1402 assert!(result.is_err());
1403 let err = result.unwrap_err();
1404 assert!(
1405 matches!(
1406 err,
1407 revm::context::result::EVMError::Transaction(
1408 TempoInvalidTransaction::ValidAfter {
1409 current: 100,
1410 valid_after: 200
1411 }
1412 )
1413 ),
1414 "Expected ValidAfter error, got: {err:?}"
1415 );
1416 }
1417
1418 {
1420 let mut evm = create_funded_evm_with_timestamp(caller, 200);
1421 let signed_tx = create_signed_tx(None, Some(200))?;
1422 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1423
1424 let result = evm.transact(tx_env);
1425 assert!(result.is_err());
1426 let err = result.unwrap_err();
1427 assert!(
1428 matches!(
1429 err,
1430 revm::context::result::EVMError::Transaction(
1431 TempoInvalidTransaction::ValidBefore {
1432 current: 200,
1433 valid_before: 200
1434 }
1435 )
1436 ),
1437 "Expected ValidBefore error, got: {err:?}"
1438 );
1439 }
1440
1441 {
1443 let mut evm = create_funded_evm_with_timestamp(caller, 300);
1444 let signed_tx = create_signed_tx(None, Some(200))?;
1445 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1446
1447 let result = evm.transact(tx_env);
1448 assert!(result.is_err());
1449 let err = result.unwrap_err();
1450 assert!(
1451 matches!(
1452 err,
1453 revm::context::result::EVMError::Transaction(
1454 TempoInvalidTransaction::ValidBefore {
1455 current: 300,
1456 valid_before: 200
1457 }
1458 )
1459 ),
1460 "Expected ValidBefore error, got: {err:?}"
1461 );
1462 }
1463
1464 {
1466 let mut evm = create_funded_evm_with_timestamp(caller, 200);
1467 let signed_tx = create_signed_tx(Some(200), None)?;
1468 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1469
1470 let result = evm.transact(tx_env)?;
1471 assert!(result.result.is_success());
1472 }
1473
1474 {
1476 let mut evm = create_funded_evm_with_timestamp(caller, 150);
1477 let signed_tx = create_signed_tx(Some(100), Some(200))?;
1478 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1479
1480 let result = evm.transact(tx_env)?;
1481 assert!(result.result.is_success());
1482 }
1483
1484 {
1486 let mut evm = create_funded_evm_with_timestamp(caller, 50);
1487 let signed_tx = create_signed_tx(Some(100), Some(200))?;
1488 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1489
1490 let result = evm.transact(tx_env);
1491 assert!(result.is_err());
1492 let err = result.unwrap_err();
1493 assert!(
1494 matches!(
1495 err,
1496 revm::context::result::EVMError::Transaction(
1497 TempoInvalidTransaction::ValidAfter {
1498 current: 50,
1499 valid_after: 100
1500 }
1501 )
1502 ),
1503 "Expected ValidAfter error, got: {err:?}"
1504 );
1505 }
1506
1507 {
1509 let mut evm = create_funded_evm_with_timestamp(caller, 200);
1510 let signed_tx = create_signed_tx(Some(100), Some(200))?;
1511 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1512
1513 let result = evm.transact(tx_env);
1514 assert!(result.is_err());
1515 let err = result.unwrap_err();
1516 assert!(
1517 matches!(
1518 err,
1519 revm::context::result::EVMError::Transaction(
1520 TempoInvalidTransaction::ValidBefore {
1521 current: 200,
1522 valid_before: 200
1523 }
1524 )
1525 ),
1526 "Expected ValidBefore error, got: {err:?}"
1527 );
1528 }
1529
1530 Ok(())
1531 }
1532
1533 #[test]
1536 fn test_tempo_tx_create_first_call() -> eyre::Result<()> {
1537 let key_pair = P256KeyPair::random();
1538 let caller = key_pair.address;
1539
1540 let initcode = vec![0x60, 0x00, 0x60, 0x00, 0xF3];
1542
1543 let tx = TxBuilder::new()
1545 .create(&initcode)
1546 .call_identity(&[0x01, 0x02])
1547 .gas_limit(200_000)
1548 .build();
1549
1550 let signed_tx = key_pair.sign_tx(tx)?;
1551 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1552
1553 let mut evm = create_funded_evm(caller);
1555 let result = evm.transact_commit(tx_env)?;
1556
1557 assert!(result.is_success(), "CREATE as first call should succeed");
1558
1559 Ok(())
1560 }
1561
1562 #[test]
1565 fn test_tempo_tx_create_second_call_fails() -> eyre::Result<()> {
1566 let key_pair = P256KeyPair::random();
1567 let caller = key_pair.address;
1568
1569 let initcode = vec![0x60, 0x00, 0x60, 0x00, 0xF3];
1571
1572 let tx = TxBuilder::new()
1574 .call_identity(&[0x01, 0x02])
1575 .create(&initcode)
1576 .gas_limit(200_000)
1577 .build();
1578
1579 let signed_tx = key_pair.sign_tx(tx)?;
1580 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1581
1582 let mut evm = create_funded_evm(caller);
1584 let result = evm.transact(tx_env);
1585
1586 assert!(result.is_err(), "CREATE as second call should fail");
1587 let err = result.unwrap_err();
1588 assert!(
1589 matches!(
1590 err,
1591 revm::context::result::EVMError::Transaction(
1592 TempoInvalidTransaction::CallsValidation(msg)
1593 ) if msg.contains("first call")
1594 ),
1595 "Expected CallsValidation error about 'first call', got: {err:?}"
1596 );
1597
1598 Ok(())
1599 }
1600
1601 #[test]
1607 fn test_validate_aa_initial_tx_gas_errors() -> eyre::Result<()> {
1608 use revm::{context::result::EVMError, handler::Handler};
1609
1610 use crate::handler::TempoEvmHandler;
1611
1612 let key_pair = P256KeyPair::random();
1613 let caller = key_pair.address;
1614
1615 let create_evm_with_tx =
1617 |tx: TempoTransaction| -> eyre::Result<TempoEvm<CacheDB<EmptyDB>, ()>> {
1618 let signed_tx = key_pair.sign_tx(tx)?;
1619 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1620 let mut evm = create_funded_evm(caller);
1621 evm.ctx.tx = tx_env;
1622 Ok(evm)
1623 };
1624
1625 let handler = TempoEvmHandler::default();
1626
1627 {
1629 let oversized_initcode = vec![0x60; 50_000];
1631
1632 let mut evm = create_evm_with_tx(
1633 TxBuilder::new()
1634 .create(&oversized_initcode)
1635 .gas_limit(10_000_000)
1636 .build(),
1637 )?;
1638
1639 let result = handler.validate_initial_tx_gas(&mut evm);
1640 assert!(
1641 matches!(
1642 result,
1643 Err(EVMError::Transaction(
1644 TempoInvalidTransaction::EthInvalidTransaction(
1645 revm::context::result::InvalidTransaction::CreateInitCodeSizeLimit
1646 )
1647 ))
1648 ),
1649 "Expected CreateInitCodeSizeLimit error, got: {result:?}"
1650 );
1651 }
1652
1653 {
1655 let mut evm = create_evm_with_tx(
1656 TxBuilder::new()
1657 .call_with_value(IDENTITY_PRECOMPILE, &[0x01, 0x02], U256::from(1000))
1658 .build(),
1659 )?;
1660
1661 let result = handler.validate_initial_tx_gas(&mut evm);
1662 assert!(
1663 matches!(
1664 result,
1665 Err(EVMError::Transaction(
1666 TempoInvalidTransaction::ValueTransferNotAllowedInAATx
1667 ))
1668 ),
1669 "Expected ValueTransferNotAllowedInAATx error, got: {result:?}"
1670 );
1671 }
1672
1673 {
1675 let mut evm = create_evm_with_tx(
1676 TxBuilder::new()
1677 .call_identity(&[0x01, 0x02, 0x03, 0x04])
1678 .gas_limit(1000) .build(),
1680 )?;
1681
1682 let result = handler.validate_initial_tx_gas(&mut evm);
1683 assert!(
1684 matches!(
1685 result,
1686 Err(EVMError::Transaction(
1687 TempoInvalidTransaction::EthInvalidTransaction(
1688 InvalidTransaction::CallGasCostMoreThanGasLimit {
1689 gas_limit: 1000,
1690 initial_gas
1691 }
1692 )
1693 )) if initial_gas > 1000
1694 ),
1695 "Expected CallGasCostMoreThanGasLimit error, got: {result:?}"
1696 );
1697 }
1698
1699 {
1706 let large_calldata = vec![0x42; 1000]; let mut evm = create_evm_with_tx(
1709 TxBuilder::new()
1710 .call_identity(&large_calldata)
1711 .gas_limit(31_000)
1712 .build(),
1713 )?;
1714
1715 let result = handler.validate_initial_tx_gas(&mut evm);
1716
1717 assert!(
1718 matches!(
1719 result,
1720 Err(EVMError::Transaction(
1721 TempoInvalidTransaction::EthInvalidTransaction(
1722 InvalidTransaction::CallGasCostMoreThanGasLimit {
1723 gas_limit: 31_000,
1724 initial_gas
1725 }
1726 )
1727 )) if initial_gas > 31_000
1728 ),
1729 "Expected CallGasCostMoreThanGasLimit, got: {result:?}"
1730 );
1731 }
1732
1733 {
1736 let large_calldata = vec![0x42; 1000];
1737
1738 let mut evm = create_evm_with_tx(
1739 TxBuilder::new()
1740 .call_identity(&large_calldata)
1741 .gas_limit(1_000_000) .build(),
1743 )?;
1744
1745 let result = handler.validate_initial_tx_gas(&mut evm);
1746 assert!(
1747 result.is_ok(),
1748 "Expected success with sufficient gas, got: {result:?}"
1749 );
1750
1751 let gas = result.unwrap();
1752 assert!(
1754 gas.floor_gas > gas.initial_total_gas(),
1755 "Expected floor_gas ({}) > initial_total_gas ({}) for large calldata",
1756 gas.floor_gas,
1757 gas.initial_total_gas()
1758 );
1759 }
1760
1761 {
1763 let mut evm = create_evm_with_tx(
1764 TxBuilder::new()
1765 .call_identity(&[0x01, 0x02, 0x03, 0x04])
1766 .gas_limit(1_000_000)
1767 .build(),
1768 )?;
1769
1770 let result = handler.validate_initial_tx_gas(&mut evm);
1771 assert!(result.is_ok(), "Expected success, got: {result:?}");
1772
1773 let gas = result.unwrap();
1774 assert!(
1775 gas.initial_total_gas() >= 21_000,
1776 "Initial gas should be at least 21k base"
1777 );
1778 }
1779
1780 Ok(())
1781 }
1782
1783 #[test]
1789 fn test_aa_tx_gas_baseline_identity_call() -> eyre::Result<()> {
1790 let key_pair = P256KeyPair::random();
1791 let caller = key_pair.address;
1792
1793 let mut evm = create_funded_evm_t1(caller);
1794
1795 let tx = TxBuilder::new()
1798 .call_identity(&[0x01, 0x02, 0x03, 0x04])
1799 .gas_limit(500_000)
1800 .build();
1801
1802 let signed_tx = key_pair.sign_tx(tx)?;
1803 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1804
1805 let result = evm.transact_commit(tx_env)?;
1806 assert!(result.is_success());
1807
1808 let gas_used = result.tx_gas_used();
1810 assert_eq!(
1811 gas_used, 278738,
1812 "T1 baseline identity call gas should be exact"
1813 );
1814
1815 Ok(())
1816 }
1817
1818 #[test]
1822 fn test_aa_tx_gas_sstore_new_slot() -> eyre::Result<()> {
1823 let key_pair = P256KeyPair::random();
1824 let caller = key_pair.address;
1825 let contract = Address::repeat_byte(0x55);
1826
1827 let mut evm = create_funded_evm_t1(caller);
1828
1829 let sstore_bytecode = Bytecode::new_raw(bytes!("60426000555B00"));
1833 evm.ctx.db_mut().insert_account_info(
1834 contract,
1835 AccountInfo {
1836 code: Some(sstore_bytecode),
1837 ..Default::default()
1838 },
1839 );
1840
1841 let tx = TxBuilder::new()
1843 .call(contract, &[])
1844 .gas_limit(600_000)
1845 .build();
1846
1847 let signed_tx = key_pair.sign_tx(tx)?;
1848 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1849
1850 let result = evm.transact_commit(tx_env)?;
1851 assert!(result.is_success(), "SSTORE transaction should succeed");
1852
1853 let gas_used = result.tx_gas_used();
1855 assert_eq!(
1856 gas_used, 530863,
1857 "T1 SSTORE to new slot gas should be exact"
1858 );
1859
1860 Ok(())
1861 }
1862
1863 #[test]
1867 fn test_aa_tx_gas_sstore_warm_slot() -> eyre::Result<()> {
1868 let key_pair = P256KeyPair::random();
1869 let caller = key_pair.address;
1870 let contract = Address::repeat_byte(0x56);
1871
1872 let mut evm = create_funded_evm_t1(caller);
1873
1874 let sstore_bytecode = Bytecode::new_raw(bytes!("60426000555B00"));
1877 evm.ctx.db_mut().insert_account_info(
1878 contract,
1879 AccountInfo {
1880 code: Some(sstore_bytecode),
1881 ..Default::default()
1882 },
1883 );
1884
1885 evm.ctx
1887 .db_mut()
1888 .insert_account_storage(contract, U256::ZERO, U256::from(1))
1889 .unwrap();
1890
1891 let tx = TxBuilder::new()
1893 .call(contract, &[])
1894 .gas_limit(500_000)
1895 .build();
1896
1897 let signed_tx = key_pair.sign_tx(tx)?;
1898 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1899
1900 let result = evm.transact_commit(tx_env)?;
1901 assert!(
1902 result.is_success(),
1903 "SSTORE to existing slot should succeed"
1904 );
1905
1906 let gas_used = result.tx_gas_used();
1909 assert_eq!(
1910 gas_used, 283663,
1911 "T1 SSTORE to existing slot gas should be exact"
1912 );
1913
1914 Ok(())
1915 }
1916
1917 #[test]
1920 fn test_aa_tx_gas_multiple_sstores() -> eyre::Result<()> {
1921 let key_pair = P256KeyPair::random();
1922 let caller = key_pair.address;
1923 let contract = Address::repeat_byte(0x57);
1924
1925 let mut evm = create_funded_evm_t1(caller);
1926
1927 let multi_sstore_bytecode = Bytecode::new_raw(bytes!("601160005560226001555B00"));
1932 evm.ctx.db_mut().insert_account_info(
1933 contract,
1934 AccountInfo {
1935 code: Some(multi_sstore_bytecode),
1936 ..Default::default()
1937 },
1938 );
1939
1940 let tx = TxBuilder::new()
1942 .call(contract, &[])
1943 .gas_limit(1_000_000)
1944 .build();
1945
1946 let signed_tx = key_pair.sign_tx(tx)?;
1947 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
1948
1949 let result = evm.transact_commit(tx_env)?;
1950 assert!(
1951 result.is_success(),
1952 "Multiple SSTORE transaction should succeed"
1953 );
1954
1955 let gas_used = result.tx_gas_used();
1957 assert_eq!(gas_used, 783069, "T1 multiple SSTOREs gas should be exact");
1958
1959 Ok(())
1960 }
1961
1962 fn seed_storage_credit_balance(
1966 evm: &mut TempoEvm<CacheDB<EmptyDB>, ()>,
1967 owner: Address,
1968 balance: u64,
1969 ) {
1970 evm.ctx
1972 .db_mut()
1973 .insert_account_info(STORAGE_CREDITS_ADDRESS, AccountInfo::default());
1974 let slot = StorageCredits::slot(owner);
1975 evm.ctx
1976 .db_mut()
1977 .insert_account_storage(STORAGE_CREDITS_ADDRESS, slot, U256::from(balance))
1978 .unwrap();
1979 }
1980
1981 fn storage_credit_word(evm: &TempoEvm<CacheDB<EmptyDB>, ()>, owner: Address) -> U256 {
1982 let slot = StorageCredits::slot(owner);
1983 evm.ctx
1984 .db()
1985 .storage_ref(STORAGE_CREDITS_ADDRESS, slot)
1986 .unwrap()
1987 }
1988
1989 fn storage_credit_balance(evm: &TempoEvm<CacheDB<EmptyDB>, ()>, owner: Address) -> u64 {
1991 u64::from_word(storage_credit_word(evm, owner)).unwrap()
1992 }
1993
1994 fn tip1060_abi_mode(mode: CreditMode) -> Mode {
1995 match mode {
1996 CreditMode::Refund => Mode::Refund,
1997 CreditMode::Preserve => Mode::Preserve,
1998 CreditMode::Direct => Mode::Direct,
1999 }
2000 }
2001
2002 fn append_tip1060_precompile_call(bytecode_bytes: &mut Vec<u8>, input_bytes: &[u8]) {
2003 append_tip1060_precompile_call_store_return(bytecode_bytes, input_bytes, None);
2004 }
2005
2006 fn append_tip1060_precompile_call_store_return(
2008 bytecode_bytes: &mut Vec<u8>,
2009 input_bytes: &[u8],
2010 store_return_in_slot: Option<u8>,
2011 ) {
2012 for (i, &byte) in input_bytes.iter().enumerate() {
2013 assert!(i <= u8::MAX as usize);
2014 bytecode_bytes.extend_from_slice(&[
2016 opcode::PUSH1,
2017 byte,
2018 opcode::PUSH1,
2019 i as u8,
2020 opcode::MSTORE8,
2021 ]);
2022 }
2023
2024 let ret_size = if store_return_in_slot.is_some() {
2025 0x20
2026 } else {
2027 0
2028 };
2029
2030 bytecode_bytes.extend_from_slice(&[opcode::PUSH1, ret_size, opcode::PUSH1, 0x00]);
2033 bytecode_bytes.extend_from_slice(&[opcode::PUSH1, input_bytes.len() as u8]);
2034 bytecode_bytes.extend_from_slice(&bytes!("60006000"));
2035 bytecode_bytes.push(opcode::PUSH20);
2037 bytecode_bytes.extend_from_slice(STORAGE_CREDITS_ADDRESS.as_slice());
2038 bytecode_bytes.extend_from_slice(&bytes!("620f4240f150"));
2040
2041 if let Some(slot) = store_return_in_slot {
2042 bytecode_bytes.extend_from_slice(&[
2044 opcode::PUSH1,
2045 0x00,
2046 opcode::MLOAD,
2047 opcode::PUSH1,
2048 slot,
2049 ]);
2050 bytecode_bytes.push(opcode::SSTORE);
2051 }
2052 }
2053
2054 fn append_tip1060_set_mode_call(bytecode_bytes: &mut Vec<u8>, mode: CreditMode) {
2056 let input_bytes = IStorageCredits::setModeCall {
2057 newMode: tip1060_abi_mode(mode),
2058 }
2059 .abi_encode();
2060
2061 append_tip1060_precompile_call(bytecode_bytes, &input_bytes);
2062 }
2063
2064 fn bytecode_with_tip1060_mode(mode: CreditMode, body: &[u8]) -> Bytecode {
2065 let mut bytecode = Vec::new();
2066 if mode != CreditMode::Refund {
2067 append_tip1060_set_mode_call(&mut bytecode, mode);
2068 }
2069 bytecode.extend_from_slice(body);
2070 Bytecode::new_raw(bytecode.into())
2071 }
2072
2073 fn run_tx_on_tip1060_contract(
2074 mode: CreditMode,
2075 contract: Address,
2076 bytecode: &[u8],
2077 ) -> eyre::Result<(u64, TempoEvm<CacheDB<EmptyDB>, ()>)> {
2078 run_tx_on_tip1060_contract_with_setup(mode, contract, bytecode, |_, _| Ok(()))
2079 }
2080
2081 fn run_tx_on_tip1060_contract_with_setup(
2082 mode: CreditMode,
2083 contract: Address,
2084 bytecode: &[u8],
2085 setup: impl FnOnce(&mut TempoEvm<CacheDB<EmptyDB>, ()>, Address) -> eyre::Result<()>,
2086 ) -> eyre::Result<(u64, TempoEvm<CacheDB<EmptyDB>, ()>)> {
2087 let key_pair = P256KeyPair::random();
2088 let caller = key_pair.address;
2089 let mut evm = create_funded_evm_t7(caller);
2090
2091 evm.ctx.db_mut().insert_account_info(
2092 contract,
2093 AccountInfo {
2094 code: Some(bytecode_with_tip1060_mode(mode, bytecode)),
2095 ..Default::default()
2096 },
2097 );
2098 setup(&mut evm, contract)?;
2099
2100 let tx = TxBuilder::new()
2101 .call(contract, &[])
2102 .gas_limit(2_000_000)
2103 .build();
2104 let signed_tx = key_pair.sign_tx(tx)?;
2105 let result = evm.transact_commit(TempoTxEnv::from_recovered_tx(&signed_tx, caller))?;
2106 assert!(result.is_success(), "test transaction should succeed");
2107
2108 Ok((result.tx_gas_used(), evm))
2109 }
2110
2111 fn mint_storage_credits_with_clears(
2112 evm: &mut TempoEvm<CacheDB<EmptyDB>, ()>,
2113 key_pair: &P256KeyPair,
2114 caller: Address,
2115 contract: Address,
2116 nonce: u64,
2117 credits: u64,
2118 ) -> eyre::Result<u64> {
2119 if credits == 0 {
2120 return Ok(nonce);
2121 }
2122
2123 let mut body = Vec::new();
2124 for credit in 0..credits {
2125 let slot = 0xf0 + credit;
2126 assert!(slot <= u64::from(u8::MAX));
2127 evm.ctx
2128 .db_mut()
2129 .insert_account_storage(contract, U256::from(slot), U256::ONE)?;
2130 body.extend_from_slice(&[opcode::PUSH1, 0, opcode::PUSH1, slot as u8, opcode::SSTORE]);
2131 }
2132 body.push(opcode::STOP);
2133
2134 evm.ctx.db_mut().insert_account_info(
2135 contract,
2136 AccountInfo {
2137 code: Some(Bytecode::new_raw(body.into())),
2138 ..Default::default()
2139 },
2140 );
2141
2142 let tx = TxBuilder::new()
2143 .call(contract, &[])
2144 .nonce(nonce)
2145 .gas_limit(2_000_000)
2146 .build();
2147 let signed_tx = key_pair.sign_tx(tx)?;
2148 let result = evm.transact_commit(TempoTxEnv::from_recovered_tx(&signed_tx, caller))?;
2149 assert!(result.is_success(), "credit-minting prelude should succeed");
2150 assert_eq!(
2151 storage_credit_balance(evm, contract),
2152 credits,
2153 "prelude should mint the requested storage credits"
2154 );
2155
2156 Ok(nonce + 1)
2157 }
2158
2159 fn branching_bytecode_with_tip1060_mode(mode: CreditMode) -> Bytecode {
2160 let mut bytecode = Vec::new();
2161 if mode != CreditMode::Refund {
2162 append_tip1060_set_mode_call(&mut bytecode, mode);
2163 }
2164
2165 let create_only_dest = bytecode.len() + 15;
2166 assert!(create_only_dest <= u8::MAX as usize);
2167
2168 bytecode.extend_from_slice(&[
2169 opcode::CALLDATASIZE,
2170 opcode::PUSH1,
2171 create_only_dest as u8,
2172 opcode::JUMPI,
2173 ]);
2174 bytecode.extend_from_slice(&bytes!("6001600055600060005500"));
2175 bytecode.push(opcode::JUMPDEST);
2176 bytecode.extend_from_slice(&bytes!("600160005500"));
2177 Bytecode::new_raw(bytecode.into())
2178 }
2179
2180 #[test]
2181 fn test_tip1060_storage_credits_delegatecall_rejected() -> eyre::Result<()> {
2182 let calldata = IStorageCredits::setModeCall {
2183 newMode: Mode::Direct,
2184 }
2185 .abi_encode();
2186
2187 for (call_opcode, contract) in [
2188 (opcode::DELEGATECALL, Address::repeat_byte(0x61)),
2189 (opcode::CALLCODE, Address::repeat_byte(0x62)),
2190 ] {
2191 let key_pair = P256KeyPair::random();
2192 let caller = key_pair.address;
2193 let mut bytecode = Vec::new();
2194 for (i, &byte) in calldata.iter().enumerate() {
2195 assert!(i <= u8::MAX as usize);
2196 bytecode.extend_from_slice(&[
2197 opcode::PUSH1,
2198 byte,
2199 opcode::PUSH1,
2200 i as u8,
2201 opcode::MSTORE8,
2202 ]);
2203 }
2204
2205 bytecode.extend_from_slice(&bytes!("60006000"));
2209 bytecode.extend_from_slice(&[opcode::PUSH1, calldata.len() as u8]);
2210 bytecode.extend_from_slice(&bytes!("6000"));
2211 if call_opcode == opcode::CALLCODE {
2212 bytecode.extend_from_slice(&bytes!("6000"));
2214 }
2215 bytecode.push(opcode::PUSH20);
2216 bytecode.extend_from_slice(STORAGE_CREDITS_ADDRESS.as_slice());
2217 bytecode.extend_from_slice(&bytes!("620f4240"));
2219 bytecode.push(call_opcode);
2220 bytecode.push(opcode::POP);
2221 bytecode.extend_from_slice(&bytes!("3d600060003e3d6000fd"));
2223
2224 let mut evm = create_funded_evm_t7(caller);
2225 evm.ctx.db_mut().insert_account_info(
2226 contract,
2227 AccountInfo {
2228 code: Some(Bytecode::new_raw(bytecode.into())),
2229 ..Default::default()
2230 },
2231 );
2232
2233 let tx = TxBuilder::new()
2234 .call(contract, &[])
2235 .gas_limit(1_000_000)
2236 .build();
2237 let signed_tx = key_pair.sign_tx(tx)?;
2238 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
2239
2240 if let ExecutionResult::Revert { output, .. } = evm.transact_commit(tx_env)? {
2241 assert_eq!(
2242 output.as_ref(),
2243 DelegateCallNotAllowed {}.abi_encode().as_slice()
2244 );
2245 } else {
2246 panic!("expected DelegateCallNotAllowed revert");
2247 }
2248 }
2249
2250 Ok(())
2251 }
2252
2253 #[test]
2258 fn test_tip1060_refund_settlement_uses_pending_field_not_mode() -> eyre::Result<()> {
2259 for mode in [CreditMode::Refund, CreditMode::Preserve, CreditMode::Direct] {
2260 let key_pair = P256KeyPair::random();
2261 let caller = key_pair.address;
2262 let contract = Address::repeat_byte(0x62);
2263
2264 let mut bytecode = bytes!("6001600055").to_vec();
2267 append_tip1060_set_mode_call(&mut bytecode, mode);
2268 bytecode.push(opcode::STOP);
2269
2270 let mut evm = create_funded_evm_t7(caller);
2271 evm.ctx.db_mut().insert_account_info(
2272 contract,
2273 AccountInfo {
2274 code: Some(Bytecode::new_raw(bytecode.into())),
2275 ..Default::default()
2276 },
2277 );
2278
2279 let tx = TxBuilder::new()
2280 .call(contract, &[])
2281 .gas_limit(2_000_000)
2282 .build();
2283 let signed_tx = key_pair.sign_tx(tx)?;
2284 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
2285
2286 let result = evm.transact_commit(tx_env)?;
2287 assert!(result.is_success());
2288
2289 assert_eq!(
2290 storage_credit_balance(&evm, contract),
2291 0,
2292 "settlement must not consume the transient mode field as storage credit balance in {mode:?} mode"
2293 );
2294 assert_eq!(
2295 storage_credit_word(&evm, contract),
2296 U256::ZERO,
2297 "mode is transient and must not persist in the storage credit state word in {mode:?} mode"
2298 );
2299 }
2300
2301 Ok(())
2302 }
2303
2304 #[test]
2309 fn test_tip1060_set_mode_uses_transient_state_only() -> eyre::Result<()> {
2310 let key_pair = P256KeyPair::random();
2311 let caller = key_pair.address;
2312 let mut evm = create_funded_evm_t7(caller);
2313 evm.ctx.db_mut().insert_account_info(
2314 caller,
2315 AccountInfo {
2316 balance: U256::from(DEFAULT_BALANCE),
2317 nonce: 1,
2318 ..Default::default()
2319 },
2320 );
2321
2322 seed_storage_credit_balance(&mut evm, STORAGE_CREDITS_ADDRESS, 1);
2324
2325 let calldata = IStorageCredits::setModeCall {
2326 newMode: Mode::Preserve,
2327 }
2328 .abi_encode();
2329
2330 let tx = TxBuilder::new()
2331 .call(STORAGE_CREDITS_ADDRESS, &calldata)
2332 .nonce(1)
2333 .gas_limit(50_000)
2335 .build();
2336 let signed_tx = key_pair.sign_tx(tx)?;
2337 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
2338
2339 let result = evm.transact_commit(tx_env)?;
2340 assert!(
2341 result.is_success(),
2342 "setMode should not need the 250k TIP-1000 storage-creation charge"
2343 );
2344 assert!(
2345 result.tx_gas_used() < 50_000,
2346 "setMode should fit under the low gas limit as a transient write"
2347 );
2348
2349 assert_eq!(
2350 storage_credit_balance(&evm, caller),
2351 0,
2352 "setMode must not mint caller credits"
2353 );
2354 assert_eq!(
2355 storage_credit_word(&evm, caller),
2356 U256::ZERO,
2357 "setMode must not create or update persistent caller state"
2358 );
2359
2360 assert_eq!(
2362 storage_credit_balance(&evm, STORAGE_CREDITS_ADDRESS),
2363 1,
2364 "storage-credits bookkeeping must not recursively consume its own storage credits"
2365 );
2366
2367 Ok(())
2368 }
2369
2370 #[test]
2375 fn test_tip1060_sstore_clear_mints_storage_credit_without_legacy_refund() -> eyre::Result<()> {
2376 let clear_bytecode = Bytecode::new_raw(bytes!("600060005500"));
2378
2379 let key_pair = P256KeyPair::random();
2380 let caller = key_pair.address;
2381 let contract = Address::repeat_byte(0x63);
2382
2383 let mut evm = create_funded_evm_t7(caller);
2384 evm.ctx.db_mut().insert_account_info(
2385 contract,
2386 AccountInfo {
2387 code: Some(clear_bytecode),
2388 ..Default::default()
2389 },
2390 );
2391 evm.ctx
2392 .db_mut()
2393 .insert_account_storage(contract, U256::ZERO, U256::ONE)?;
2394
2395 let tx = TxBuilder::new()
2396 .call(contract, &[])
2397 .gas_limit(2_000_000)
2398 .build();
2399 let signed_tx = key_pair.sign_tx(tx)?;
2400 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
2401
2402 let result = evm.transact_commit(tx_env)?;
2403 assert!(result.is_success(), "clear tx should succeed");
2404 assert_eq!(
2405 result.gas().inner_refunded(),
2406 0,
2407 "TIP-1060 removes the legacy SSTORE clearing refund"
2408 );
2409 assert_eq!(
2410 storage_credit_balance(&evm, contract),
2411 1,
2412 "clearing a nonzero slot should mint one storage credit"
2413 );
2414
2415 Ok(())
2416 }
2417
2418 #[test]
2423 fn test_tip1060_sstore_create_then_clear_modes() -> eyre::Result<()> {
2424 let noop_body = bytes!("600060005500");
2426 let (noop_gas, _) =
2427 run_tx_on_tip1060_contract(CreditMode::Refund, Address::repeat_byte(0x60), &noop_body)?;
2428
2429 let create_clear_body = bytes!("6001600055600060005500");
2431
2432 let cases = [
2436 (CreditMode::Refund, 285_968u64, 0u64),
2437 (CreditMode::Preserve, 534_133u64, 1u64),
2438 (CreditMode::Direct, 534_133u64, 1u64),
2439 ];
2440
2441 for (case_id, (mode, expected_gas, expected_balance)) in cases.into_iter().enumerate() {
2442 let contract = Address::repeat_byte(0x60 + case_id as u8);
2443 let (gas_used, evm) = run_tx_on_tip1060_contract(mode, contract, &create_clear_body)?;
2444
2445 assert!(
2446 gas_used >= noop_gas,
2447 "0->x->0 storage churn must not reduce tx gas below no-op: noop={noop_gas}, create_clear={gas_used}"
2448 );
2449
2450 assert_eq!(
2451 gas_used, expected_gas,
2452 "TIP-1060 create+clear gas should be exact in {mode:?} mode"
2453 );
2454
2455 assert_eq!(
2456 storage_credit_balance(&evm, contract),
2457 expected_balance,
2458 "TIP-1060 post-tx storage credit balance should be exact in {mode:?} mode"
2459 );
2460 }
2461
2462 Ok(())
2463 }
2464
2465 #[test]
2467 fn test_tip1060_spec_transition_classes_credit_accounting_table() -> eyre::Result<()> {
2468 fn append_sstore(body: &mut Vec<u8>, slot: u8, value: u8) {
2469 body.extend_from_slice(&[opcode::PUSH1, value, opcode::PUSH1, slot, opcode::SSTORE]);
2470 }
2471
2472 fn transition_body(setup_value: Option<u8>, new_value: u8) -> Bytes {
2473 let mut body = Vec::new();
2474 if let Some(value) = setup_value {
2475 append_sstore(&mut body, 0, value);
2476 }
2477 append_sstore(&mut body, 0, new_value);
2478 body.push(opcode::STOP);
2479 body.into()
2480 }
2481
2482 #[derive(Clone, Copy)]
2483 struct ModeExpectation {
2484 mode: CreditMode,
2485 expected_gas: u64,
2486 expected_credits: u64,
2487 }
2488
2489 struct CreditCase {
2490 name: &'static str,
2491 initial_credits: u64,
2492 expectations: [ModeExpectation; 3],
2493 }
2494
2495 struct TransitionClass {
2496 name: &'static str,
2497 original: u8,
2498 setup_value: Option<u8>,
2499 new_value: u8,
2500 expected_slot: u64,
2501 credit_cases: Vec<CreditCase>,
2502 }
2503
2504 fn expectations(
2505 refund: (u64, u64),
2506 preserve: (u64, u64),
2507 direct: (u64, u64),
2508 ) -> [ModeExpectation; 3] {
2509 [
2510 ModeExpectation {
2511 mode: CreditMode::Refund,
2512 expected_gas: refund.0,
2513 expected_credits: refund.1,
2514 },
2515 ModeExpectation {
2516 mode: CreditMode::Preserve,
2517 expected_gas: preserve.0,
2518 expected_credits: preserve.1,
2519 },
2520 ModeExpectation {
2521 mode: CreditMode::Direct,
2522 expected_gas: direct.0,
2523 expected_credits: direct.1,
2524 },
2525 ]
2526 }
2527
2528 const SET_MODE_GAS: u64 = 3_165;
2529
2530 fn same_storage_accounting(refund_gas: u64, expected_credits: u64) -> [ModeExpectation; 3] {
2531 expectations(
2532 (refund_gas, expected_credits),
2533 (refund_gas + SET_MODE_GAS, expected_credits),
2534 (refund_gas + SET_MODE_GAS, expected_credits),
2535 )
2536 }
2537
2538 fn no_initial_credits(expectations: [ModeExpectation; 3]) -> Vec<CreditCase> {
2539 vec![CreditCase {
2540 name: "no initial credits",
2541 initial_credits: 0,
2542 expectations,
2543 }]
2544 }
2545
2546 let transition_classes = [
2547 TransitionClass {
2548 name: "O=0 P=0 N=0 no-op",
2549 original: 0,
2550 setup_value: None,
2551 new_value: 0,
2552 expected_slot: 0,
2553 credit_cases: no_initial_credits(same_storage_accounting(30_862, 0)),
2554 },
2555 TransitionClass {
2556 name: "O=0 P=Y N=Y same-tx no-op",
2557 original: 0,
2558 setup_value: Some(2),
2559 new_value: 2,
2560 expected_slot: 2,
2561 credit_cases: no_initial_credits(same_storage_accounting(283_068, 0)),
2562 },
2563 TransitionClass {
2564 name: "O=0 P=Y N=Z dirty overwrite",
2565 original: 0,
2566 setup_value: Some(2),
2567 new_value: 3,
2568 expected_slot: 3,
2569 credit_cases: no_initial_credits(same_storage_accounting(283_068, 0)),
2570 },
2571 TransitionClass {
2572 name: "O=X P=X N=X no-op",
2573 original: 1,
2574 setup_value: None,
2575 new_value: 1,
2576 expected_slot: 1,
2577 credit_cases: no_initial_credits(same_storage_accounting(30_862, 0)),
2578 },
2579 TransitionClass {
2580 name: "O=X P=X N=Y clean overwrite",
2581 original: 1,
2582 setup_value: None,
2583 new_value: 2,
2584 expected_slot: 2,
2585 credit_cases: no_initial_credits(same_storage_accounting(33_662, 0)),
2586 },
2587 TransitionClass {
2588 name: "O=X P=Y N=X dirty restore to original",
2589 original: 1,
2590 setup_value: Some(2),
2591 new_value: 1,
2592 expected_slot: 1,
2593 credit_cases: no_initial_credits(same_storage_accounting(30_968, 0)),
2594 },
2595 TransitionClass {
2596 name: "O=X P=Y N=Z dirty overwrite",
2597 original: 1,
2598 setup_value: Some(2),
2599 new_value: 3,
2600 expected_slot: 3,
2601 credit_cases: no_initial_credits(same_storage_accounting(33_768, 0)),
2602 },
2603 TransitionClass {
2604 name: "O=0 P=Y N=0 clear of same-tx creation",
2605 original: 0,
2606 setup_value: Some(2),
2607 new_value: 0,
2608 expected_slot: 0,
2609 credit_cases: no_initial_credits(expectations(
2610 (35_968, 0),
2611 (284_133, 1),
2612 (284_133, 1),
2613 )),
2614 },
2615 TransitionClass {
2616 name: "O=X P=X N=0 clean clear",
2617 original: 1,
2618 setup_value: None,
2619 new_value: 0,
2620 expected_slot: 0,
2621 credit_cases: no_initial_credits(same_storage_accounting(38_562, 1)),
2622 },
2623 TransitionClass {
2624 name: "O=X P=Y N=0 dirty clear",
2625 original: 1,
2626 setup_value: Some(2),
2627 new_value: 0,
2628 expected_slot: 0,
2629 credit_cases: no_initial_credits(same_storage_accounting(38_668, 1)),
2630 },
2631 TransitionClass {
2632 name: "O=0 P=0 N=Y clean creation",
2633 original: 0,
2634 setup_value: None,
2635 new_value: 2,
2636 expected_slot: 2,
2637 credit_cases: vec![
2638 CreditCase {
2639 name: "no initial credits",
2640 initial_credits: 0,
2641 expectations: same_storage_accounting(282_962, 0),
2642 },
2643 CreditCase {
2644 name: "one initial credit",
2645 initial_credits: 1,
2646 expectations: expectations(
2647 (37_962, 0),
2648 (37_962 + SET_MODE_GAS + STORAGE_CREDIT_VALUE, 1),
2649 (43_927, 0),
2650 ),
2651 },
2652 CreditCase {
2653 name: "surplus initial credits",
2654 initial_credits: 2,
2655 expectations: expectations(
2656 (37_962, 1),
2657 (37_962 + SET_MODE_GAS + STORAGE_CREDIT_VALUE, 2),
2658 (43_927, 1),
2659 ),
2660 },
2661 ],
2662 },
2663 TransitionClass {
2664 name: "O=X P=0 N=X recreation to original",
2665 original: 1,
2666 setup_value: Some(0),
2667 new_value: 1,
2668 expected_slot: 1,
2669 credit_cases: vec![
2670 CreditCase {
2671 name: "no initial credits",
2672 initial_credits: 0,
2673 expectations: expectations(
2674 (35_968, 0),
2675 (35_968 + SET_MODE_GAS + STORAGE_CREDIT_VALUE, 1),
2676 (35_968 + SET_MODE_GAS, 0),
2677 ),
2678 },
2679 CreditCase {
2680 name: "surplus initial credits",
2681 initial_credits: 2,
2682 expectations: expectations(
2683 (35_968, 2),
2684 (35_968 + SET_MODE_GAS + STORAGE_CREDIT_VALUE, 3),
2685 (35_968 + SET_MODE_GAS, 2),
2686 ),
2687 },
2688 ],
2689 },
2690 TransitionClass {
2691 name: "O=X P=0 N=Y recreation to other value",
2692 original: 1,
2693 setup_value: Some(0),
2694 new_value: 2,
2695 expected_slot: 2,
2696 credit_cases: vec![
2697 CreditCase {
2698 name: "no initial credits",
2699 initial_credits: 0,
2700 expectations: expectations(
2701 (38_768, 0),
2702 (38_768 + SET_MODE_GAS + STORAGE_CREDIT_VALUE, 1),
2703 (38_768 + SET_MODE_GAS, 0),
2704 ),
2705 },
2706 CreditCase {
2707 name: "surplus initial credits",
2708 initial_credits: 2,
2709 expectations: expectations(
2710 (38_768, 2),
2711 (38_768 + SET_MODE_GAS + STORAGE_CREDIT_VALUE, 3),
2712 (38_768 + SET_MODE_GAS, 2),
2713 ),
2714 },
2715 ],
2716 },
2717 ];
2718
2719 let mut case_id = 0u8;
2720 for scenario in transition_classes {
2721 for credit_case in &scenario.credit_cases {
2722 for expectation in credit_case.expectations {
2723 let mode = expectation.mode;
2724 let key_pair = P256KeyPair::random();
2725 let caller = key_pair.address;
2726 let contract = Address::repeat_byte(0x80 + case_id);
2727 case_id += 1;
2728 let body = transition_body(scenario.setup_value, scenario.new_value);
2729
2730 let mut evm = create_funded_evm_t7(caller);
2731 fund_account_with_nonce(&mut evm, caller, 1);
2732 if scenario.original != 0 {
2733 evm.ctx.db_mut().insert_account_storage(
2734 contract,
2735 U256::ZERO,
2736 U256::from(scenario.original),
2737 )?;
2738 }
2739 let nonce = mint_storage_credits_with_clears(
2740 &mut evm,
2741 &key_pair,
2742 caller,
2743 contract,
2744 1,
2745 credit_case.initial_credits,
2746 )?;
2747
2748 evm.ctx.db_mut().insert_account_info(
2749 contract,
2750 AccountInfo {
2751 code: Some(bytecode_with_tip1060_mode(mode, &body)),
2752 ..Default::default()
2753 },
2754 );
2755
2756 let tx = TxBuilder::new()
2757 .call(contract, &[])
2758 .nonce(nonce)
2759 .gas_limit(2_000_000)
2760 .build();
2761 let signed_tx = key_pair.sign_tx(tx)?;
2762 let result =
2763 evm.transact_commit(TempoTxEnv::from_recovered_tx(&signed_tx, caller))?;
2764 assert!(
2765 result.is_success(),
2766 "{} / {} in {mode:?} should succeed",
2767 scenario.name,
2768 credit_case.name
2769 );
2770
2771 assert_eq!(
2772 result.tx_gas_used(),
2773 expectation.expected_gas,
2774 "{} / {} gas should stay exact in {mode:?}",
2775 scenario.name,
2776 credit_case.name
2777 );
2778 assert_eq!(
2779 storage_credit_balance(&evm, contract),
2780 expectation.expected_credits,
2781 "{} / {} final persistent credit balance should stay exact in {mode:?}",
2782 scenario.name,
2783 credit_case.name
2784 );
2785 assert_eq!(
2786 evm.ctx.db().storage_ref(contract, U256::ZERO)?,
2787 U256::from(scenario.expected_slot),
2788 "{} / {} should leave the expected slot value in {mode:?}",
2789 scenario.name,
2790 credit_case.name
2791 );
2792 }
2793 }
2794 }
2795
2796 Ok(())
2797 }
2798
2799 #[test]
2806 fn test_tip1060_preserve_churn_attack() -> eyre::Result<()> {
2807 use alloy_primitives::{Address, Bytes, TxKind, U256, hex};
2808 use revm::{
2809 Context, Database, ExecuteCommitEvm, MainContext,
2810 context::{CfgEnv, TxEnv},
2811 database::{CacheDB, EmptyDB},
2812 state::AccountInfo,
2813 };
2814 use tempo_chainspec::hardfork::TempoHardfork;
2815 use tempo_precompiles::{STORAGE_CREDITS_ADDRESS, storage_credits::StorageCredits};
2816
2817 use crate::{TempoBlockEnv, TempoEvm, gas_params::tempo_gas_params};
2818
2819 let init = Bytes::from(
2824 hex!(
2825 "60016000556100a660136000396100a66000f3\
2826 60216000536017600153605b600253604a6003536001602353\
2827 6000600060246000600073106000000000000000000000000000000000000\
2828 05af1506101f45b60006000556002600055600190038061003e5750\
2829 60216000536017600153605b600253604a6003536002602353\
2830 6000600060246000600073106000000000000000000000000000000000000\
2831 05af1506101f45b8061010001600190556001900380610091575000"
2832 )
2833 .to_vec(),
2834 );
2835
2836 let caller = Address::repeat_byte(0x11);
2837 let mut cfg = CfgEnv::<TempoHardfork>::default();
2838 cfg.spec = TempoHardfork::T7;
2839 cfg.gas_params = tempo_gas_params(TempoHardfork::T7);
2840 const GAS_LIMIT: u64 = 16_777_216;
2841 let mut block = TempoBlockEnv::default();
2842 block.inner.gas_limit = GAS_LIMIT;
2843 let ctx = Context::mainnet()
2844 .with_db(CacheDB::new(EmptyDB::new()))
2845 .with_block(block)
2846 .with_cfg(cfg)
2847 .with_tx(Default::default());
2848 let mut evm = TempoEvm::new(ctx, ());
2849 evm.ctx.db_mut().insert_account_info(
2850 caller,
2851 AccountInfo {
2852 balance: U256::from(1_000_000_000_000_000_000u128),
2853 ..Default::default()
2854 },
2855 );
2856
2857 let deploy = evm.transact_commit(
2859 TxEnv {
2860 caller,
2861 kind: TxKind::Create,
2862 data: init,
2863 gas_limit: GAS_LIMIT,
2864 ..Default::default()
2865 }
2866 .into(),
2867 )?;
2868 assert!(deploy.is_success(), "deploy reverted/halted: {deploy:?}");
2869 let contract = deploy
2870 .created_address()
2871 .expect("CREATE should yield an address");
2872
2873 let call = evm.transact_commit(
2875 TxEnv {
2876 caller,
2877 nonce: 1,
2878 kind: TxKind::Call(contract),
2879 gas_limit: GAS_LIMIT,
2880 ..Default::default()
2881 }
2882 .into(),
2883 )?;
2884
2885 let balance = evm
2886 .ctx
2887 .db_mut()
2888 .storage(STORAGE_CREDITS_ADDRESS, StorageCredits::slot(contract))?
2889 .as_limbs()[0];
2890 let slots = evm
2891 .ctx
2892 .db_mut()
2893 .cache
2894 .accounts
2895 .get(&contract)
2896 .map(|a| a.storage.iter().filter(|(_, v)| !v.is_zero()).count())
2897 .unwrap_or(0);
2898 eprintln!(
2899 "tx#2 success/gas: {}/{} slots: {slots} bal: {balance}",
2900 call.is_success(),
2901 call.tx_gas_used()
2902 );
2903
2904 assert!(!call.is_success());
2907 assert_eq!(slots, 1);
2908 assert_eq!(balance, 0);
2909 Ok(())
2910 }
2911
2912 #[test]
2920 fn test_tip1060_preserve_churn_mints_one_credit_per_clear() -> eyre::Result<()> {
2921 let key_pair = P256KeyPair::random();
2922 let caller = key_pair.address;
2923 let contract = Address::repeat_byte(0x6c);
2924
2925 let mut body = Vec::new();
2927 for _ in 0..3 {
2928 body.extend_from_slice(&bytes!("6000600055")); body.extend_from_slice(&bytes!("6002600055")); }
2931 body.push(opcode::STOP);
2932
2933 let mut evm = create_funded_evm_t7(caller);
2934 evm.ctx.db_mut().insert_account_info(
2935 contract,
2936 AccountInfo {
2937 code: Some(bytecode_with_tip1060_mode(CreditMode::Preserve, &body)),
2938 ..Default::default()
2939 },
2940 );
2941 evm.ctx
2943 .db_mut()
2944 .insert_account_storage(contract, U256::ZERO, U256::from(1))
2945 .unwrap();
2946 seed_storage_credit_balance(&mut evm, contract, 0);
2947
2948 let tx = TxBuilder::new()
2949 .call(contract, &[])
2950 .gas_limit(2_000_000)
2951 .build();
2952 let result = evm.transact_commit(TempoTxEnv::from_recovered_tx(
2953 &key_pair.sign_tx(tx)?,
2954 caller,
2955 ))?;
2956 assert!(result.is_success(), "preserve churn tx should succeed");
2957 assert_eq!(
2958 result.tx_gas_used(),
2959 1_027_757,
2960 "three Preserve churn cycles pay the full 245k creditable portion per recreation"
2961 );
2962
2963 assert_eq!(
2964 storage_credit_balance(&evm, contract),
2965 3,
2966 "each clear mints a credit and Preserve recreations pay 245k without consuming, so \
2967 three churn cycles accumulate three credits"
2968 );
2969 Ok(())
2970 }
2971
2972 #[test]
2977 fn test_tip1060_dirty_restore_after_direct_spend_repays_credit_value() -> eyre::Result<()> {
2978 let key_pair = P256KeyPair::random();
2979 let caller = key_pair.address;
2980 let contract = Address::repeat_byte(0x6d);
2981
2982 let mut body = Vec::new();
2984 body.extend_from_slice(&bytes!("6000600055")); body.extend_from_slice(&bytes!("6001600155")); body.extend_from_slice(&bytes!("6002600055")); body.push(opcode::STOP);
2988
2989 let mut evm = create_funded_evm_t7(caller);
2990 evm.ctx.db_mut().insert_account_info(
2991 contract,
2992 AccountInfo {
2993 code: Some(bytecode_with_tip1060_mode(CreditMode::Direct, &body)),
2994 ..Default::default()
2995 },
2996 );
2997 evm.ctx
2998 .db_mut()
2999 .insert_account_storage(contract, U256::ZERO, U256::from(1))
3000 .unwrap();
3001 seed_storage_credit_balance(&mut evm, contract, 0);
3002
3003 let tx = TxBuilder::new()
3004 .call(contract, &[])
3005 .gas_limit(2_000_000)
3006 .build();
3007 let result = evm.transact_commit(TempoTxEnv::from_recovered_tx(
3008 &key_pair.sign_tx(tx)?,
3009 caller,
3010 ))?;
3011 assert!(result.is_success(), "direct reorder tx should succeed");
3012
3013 assert_eq!(
3015 evm.ctx.db().storage_ref(contract, U256::from(1)).unwrap(),
3016 U256::from(1),
3017 "the genuinely new slot must be created"
3018 );
3019 assert_eq!(
3020 evm.ctx.db().storage_ref(contract, U256::ZERO).unwrap(),
3021 U256::from(2),
3022 "the churned slot must be restored"
3023 );
3024 assert_eq!(
3025 storage_credit_balance(&evm, contract),
3026 0,
3027 "balance must net to zero after mint + Direct spend + dirty-restore repay"
3028 );
3029 assert!(
3030 result.tx_gas_used() > STORAGE_CREDIT_VALUE,
3031 "the dirty restore must repay the {STORAGE_CREDIT_VALUE} credit value, so the new slot \
3032 costs full price; got {} gas",
3033 result.tx_gas_used()
3034 );
3035 Ok(())
3036 }
3037
3038 #[test]
3043 fn test_tip1060_minted_storage_credits_affect_second_tx() -> eyre::Result<()> {
3044 let cases = [
3047 (CreditMode::Refund, 282_994u64, 0u64, 0u64),
3048 (CreditMode::Preserve, 286_159u64, 1u64, 1u64),
3049 (CreditMode::Direct, 43_959u64, 1u64, 0u64),
3050 ];
3051
3052 for (mode, expected_second_gas, expected_credit_tx1, expected_credit_tx2) in cases {
3053 let key_pair = P256KeyPair::random();
3054 let caller = key_pair.address;
3055 let contract = Address::repeat_byte(0x61);
3056
3057 let mut evm = create_funded_evm_t7(caller);
3058
3059 evm.ctx.db_mut().insert_account_info(
3060 contract,
3061 AccountInfo {
3062 code: Some(branching_bytecode_with_tip1060_mode(mode)),
3063 ..Default::default()
3064 },
3065 );
3066 seed_storage_credit_balance(&mut evm, contract, 0);
3067
3068 let tx1 = TxBuilder::new()
3070 .call(contract, &[])
3071 .nonce(0)
3072 .gas_limit(2_000_000)
3073 .build();
3074 let signed_tx1 = key_pair.sign_tx(tx1)?;
3075 let tx_env1 = TempoTxEnv::from_recovered_tx(&signed_tx1, caller);
3076 let result1 = evm.transact_commit(tx_env1)?;
3077 assert!(
3078 result1.is_success(),
3079 "minting tx should succeed in {mode:?} mode"
3080 );
3081 assert_eq!(
3082 storage_credit_balance(&evm, contract),
3083 expected_credit_tx1,
3084 "storage credit balance after the minting tx should be exact in {mode:?} mode"
3085 );
3086
3087 let tx2 = TxBuilder::new()
3089 .call(contract, &[0x01])
3090 .nonce(1)
3091 .gas_limit(2_000_000)
3092 .build();
3093 let signed_tx2 = key_pair.sign_tx(tx2)?;
3094 let tx_env2 = TempoTxEnv::from_recovered_tx(&signed_tx2, caller);
3095 let result2 = evm.transact_commit(tx_env2)?;
3096 assert!(
3097 result2.is_success(),
3098 "create-only tx should succeed in {mode:?} mode"
3099 );
3100
3101 let second_gas = result2.tx_gas_used();
3102 assert_eq!(
3103 second_gas, expected_second_gas,
3104 "TIP-1060 second-tx create gas should be exact in {mode:?} mode"
3105 );
3106
3107 assert_eq!(
3108 storage_credit_balance(&evm, contract),
3109 expected_credit_tx2,
3110 "storage credit balance after the create-only tx should be exact in {mode:?} mode"
3111 );
3112 }
3113
3114 Ok(())
3115 }
3116
3117 #[test]
3123 fn test_tip1060_direct_budget_caps_credit_consumption() -> eyre::Result<()> {
3124 let key_pair = P256KeyPair::random();
3125 let caller = key_pair.address;
3126 let budgeted_contract = Address::repeat_byte(0x86);
3127 let unlimited_contract = Address::repeat_byte(0x87);
3128
3129 let mut budgeted_bytecode = Vec::new();
3130 let set_budget_input = IStorageCredits::setBudgetCall { creditBudget: 1 }.abi_encode();
3131 append_tip1060_precompile_call(&mut budgeted_bytecode, &set_budget_input);
3132 budgeted_bytecode.extend_from_slice(&bytes!("6001600055600160015500"));
3133
3134 let unlimited_bytecode =
3135 bytecode_with_tip1060_mode(CreditMode::Direct, &bytes!("6001600055600160015500"));
3136
3137 let mut evm = create_funded_evm_t7(caller);
3138 evm.ctx.db_mut().insert_account_info(
3139 budgeted_contract,
3140 AccountInfo {
3141 code: Some(Bytecode::new_raw(budgeted_bytecode.into())),
3142 ..Default::default()
3143 },
3144 );
3145 evm.ctx.db_mut().insert_account_info(
3146 unlimited_contract,
3147 AccountInfo {
3148 code: Some(unlimited_bytecode),
3149 ..Default::default()
3150 },
3151 );
3152 seed_storage_credit_balance(&mut evm, budgeted_contract, 2);
3153 seed_storage_credit_balance(&mut evm, unlimited_contract, 2);
3154
3155 let budgeted_tx = TxBuilder::new()
3156 .call(budgeted_contract, &[])
3157 .nonce(0)
3158 .gas_limit(2_000_000)
3159 .build();
3160 let budgeted_result = evm.transact_commit(TempoTxEnv::from_recovered_tx(
3161 &key_pair.sign_tx(budgeted_tx)?,
3162 caller,
3163 ))?;
3164 assert!(budgeted_result.is_success());
3165 assert_eq!(
3166 storage_credit_balance(&evm, budgeted_contract),
3167 1,
3168 "budget 1 must consume exactly one of the two available credits"
3169 );
3170
3171 let unlimited_tx = TxBuilder::new()
3172 .call(unlimited_contract, &[])
3173 .nonce(1)
3174 .gas_limit(2_000_000)
3175 .build();
3176 let unlimited_result = evm.transact_commit(TempoTxEnv::from_recovered_tx(
3177 &key_pair.sign_tx(unlimited_tx)?,
3178 caller,
3179 ))?;
3180 assert!(unlimited_result.is_success());
3181 assert_eq!(
3182 storage_credit_balance(&evm, unlimited_contract),
3183 0,
3184 "setMode(Direct) has unlimited budget and must consume both available credits"
3185 );
3186 assert!(
3187 budgeted_result.tx_gas_used() > unlimited_result.tx_gas_used(),
3188 "the budgeted second create should pay full creation gas after the Direct budget is exhausted"
3189 );
3190
3191 Ok(())
3192 }
3193
3194 #[test]
3195 fn test_tip1060_exhausted_direct_budget_stays_direct() -> eyre::Result<()> {
3196 const MODE_SLOT: u8 = 3;
3197 const EXHAUSTED_GAS: (u8, u8) = (4, 5);
3198 const AFTER_CLEAR_GAS: (u8, u8) = (6, 7);
3199
3200 let store_gas = |bytecode: &mut Vec<u8>, slot: u8| {
3201 bytecode.extend_from_slice(&[opcode::GAS, opcode::PUSH1, slot, opcode::SSTORE]);
3202 };
3203 let contract = Address::repeat_byte(0x88);
3204 let mut bytecode = Vec::new();
3205
3206 append_tip1060_precompile_call(
3207 &mut bytecode,
3208 &IStorageCredits::setBudgetCall { creditBudget: 1 }.abi_encode(),
3209 );
3210 bytecode.extend_from_slice(&bytes!("6001600055")); append_tip1060_precompile_call_store_return(
3212 &mut bytecode,
3213 &IStorageCredits::modeOfCall { account: contract }.abi_encode(),
3214 Some(MODE_SLOT),
3215 );
3216 store_gas(&mut bytecode, EXHAUSTED_GAS.0);
3217 bytecode.extend_from_slice(&bytes!("6001600155")); store_gas(&mut bytecode, EXHAUSTED_GAS.1);
3219 bytecode.extend_from_slice(&bytes!("6000600155")); store_gas(&mut bytecode, AFTER_CLEAR_GAS.0);
3221 bytecode.extend_from_slice(&bytes!("6001600255")); store_gas(&mut bytecode, AFTER_CLEAR_GAS.1);
3223 bytecode.push(opcode::STOP);
3224
3225 let (_, evm) = run_tx_on_tip1060_contract_with_setup(
3226 CreditMode::Refund,
3227 contract,
3228 &bytecode,
3229 |evm, contract| {
3230 seed_storage_credit_balance(evm, contract, 2);
3231 for slot in MODE_SLOT..=AFTER_CLEAR_GAS.1 {
3232 evm.ctx.db_mut().insert_account_storage(
3233 contract,
3234 U256::from(slot),
3235 U256::ONE,
3236 )?;
3237 }
3238 Ok(())
3239 },
3240 )?;
3241
3242 let word = |slot: u8| {
3243 evm.ctx
3244 .db()
3245 .storage_ref(contract, U256::from(slot))
3246 .unwrap()
3247 };
3248 let delta = |slots: (u8, u8)| word(slots.0).as_limbs()[0] - word(slots.1).as_limbs()[0];
3249
3250 assert!(
3251 delta(EXHAUSTED_GAS) > STORAGE_CREDIT_VALUE
3252 && delta(AFTER_CLEAR_GAS) > STORAGE_CREDIT_VALUE,
3253 "both exhausted creates must pay full creditable gas"
3254 );
3255 let expected = (U256::from(CreditMode::Direct as u8), U256::ZERO, U256::ONE);
3256 assert_eq!((word(MODE_SLOT), word(1), word(2)), expected);
3257 assert_eq!(storage_credit_balance(&evm, contract), 2);
3258
3259 Ok(())
3260 }
3261
3262 #[test]
3263 fn test_tip1060_reverted_scopes_unwind_credit_accounting() -> eyre::Result<()> {
3264 struct RevertedCase {
3265 name: &'static str,
3266 callee: Address,
3267 bytecode: Bytecode,
3268 initial_slot: U256,
3269 initial_credits: u64,
3270 expected_slot: U256,
3271 expected_credits: u64,
3272 }
3273
3274 fn caller_ignoring_reverted_callee(callee: Address) -> Bytecode {
3275 let mut bytecode = bytes!("60006000600060006000").to_vec();
3276 bytecode.push(opcode::PUSH20);
3277 bytecode.extend_from_slice(callee.as_slice());
3278 bytecode.extend_from_slice(&bytes!("620f4240f15000"));
3279 Bytecode::new_raw(bytecode.into())
3280 }
3281
3282 let mut direct_create_revert = Vec::new();
3283 append_tip1060_set_mode_call(&mut direct_create_revert, CreditMode::Direct);
3284 direct_create_revert.extend_from_slice(&bytes!("600160005560006000fd"));
3285
3286 let cases = [
3287 RevertedCase {
3288 name: "clear mint",
3289 callee: Address::repeat_byte(0x89),
3290 bytecode: Bytecode::new_raw(bytes!("600060005560006000fd")),
3292 initial_slot: U256::ONE,
3293 initial_credits: 0,
3294 expected_slot: U256::ONE,
3295 expected_credits: 0,
3296 },
3297 RevertedCase {
3298 name: "Refund pending creation",
3299 callee: Address::repeat_byte(0x8a),
3300 bytecode: Bytecode::new_raw(bytes!("600160005560006000fd")),
3302 initial_slot: U256::ZERO,
3303 initial_credits: 1,
3304 expected_slot: U256::ZERO,
3305 expected_credits: 1,
3306 },
3307 RevertedCase {
3308 name: "Direct debit",
3309 callee: Address::repeat_byte(0x8b),
3310 bytecode: Bytecode::new_raw(direct_create_revert.into()),
3311 initial_slot: U256::ZERO,
3312 initial_credits: 1,
3313 expected_slot: U256::ZERO,
3314 expected_credits: 1,
3315 },
3316 ];
3317
3318 for case in cases {
3319 let key_pair = P256KeyPair::random();
3320 let caller = key_pair.address;
3321 let caller_contract = Address::repeat_byte(case.callee[0] ^ 0xff);
3322 let mut evm = create_funded_evm_t7(caller);
3323
3324 evm.ctx.db_mut().insert_account_info(
3325 caller_contract,
3326 AccountInfo {
3327 code: Some(caller_ignoring_reverted_callee(case.callee)),
3328 ..Default::default()
3329 },
3330 );
3331 evm.ctx.db_mut().insert_account_info(
3332 case.callee,
3333 AccountInfo {
3334 code: Some(case.bytecode.clone()),
3335 ..Default::default()
3336 },
3337 );
3338 if !case.initial_slot.is_zero() {
3339 evm.ctx.db_mut().insert_account_storage(
3340 case.callee,
3341 U256::ZERO,
3342 case.initial_slot,
3343 )?;
3344 }
3345 seed_storage_credit_balance(&mut evm, case.callee, case.initial_credits);
3346
3347 let tx = TxBuilder::new()
3348 .call(caller_contract, &[])
3349 .gas_limit(2_000_000)
3350 .build();
3351 let result = evm.transact_commit(TempoTxEnv::from_recovered_tx(
3352 &key_pair.sign_tx(tx)?,
3353 caller,
3354 ))?;
3355 assert!(
3356 result.is_success(),
3357 "top-level caller should ignore the reverted {} subcall",
3358 case.name
3359 );
3360
3361 assert_eq!(
3362 evm.ctx.db().storage_ref(case.callee, U256::ZERO)?,
3363 case.expected_slot,
3364 "reverted {} storage write must unwind",
3365 case.name
3366 );
3367 assert_eq!(
3368 storage_credit_balance(&evm, case.callee),
3369 case.expected_credits,
3370 "reverted {} credit accounting must unwind",
3371 case.name
3372 );
3373 }
3374
3375 Ok(())
3376 }
3377
3378 #[test]
3383 fn test_tip1060_refund_settlement_min_pending_balance() -> eyre::Result<()> {
3384 let cases = [(2u8, 1u64, 0u64), (1u8, 2u64, 1u64)];
3386
3387 for (creates, starting_balance, expected_balance) in cases {
3388 let contract = Address::repeat_byte(0x70 + creates);
3389
3390 let mut bytecode = Vec::new();
3392 for slot in 0..creates {
3393 bytecode.extend_from_slice(&[
3394 opcode::PUSH1,
3395 0x01,
3396 opcode::PUSH1,
3397 slot,
3398 opcode::SSTORE,
3399 ]);
3400 }
3401 bytecode.push(opcode::STOP);
3402
3403 let (_, evm) = run_tx_on_tip1060_contract_with_setup(
3404 CreditMode::Refund,
3405 contract,
3406 &bytecode,
3407 |evm, contract| {
3408 seed_storage_credit_balance(evm, contract, starting_balance);
3409 Ok(())
3410 },
3411 )?;
3412
3413 assert_eq!(
3414 storage_credit_balance(&evm, contract),
3415 expected_balance,
3416 "settlement must consume exactly min(pending, balance) storage credits"
3417 );
3418 }
3419
3420 Ok(())
3421 }
3422
3423 #[test]
3428 fn test_tip1060_refund_settlement_is_per_account() -> eyre::Result<()> {
3429 let key_pair = P256KeyPair::random();
3430 let caller = key_pair.address;
3431 let account_a = Address::repeat_byte(0xa0);
3432 let account_b = Address::repeat_byte(0xb0);
3433
3434 let mut account_a_bytecode = bytes!("600160005560006000600060006000").to_vec();
3436 account_a_bytecode.push(opcode::PUSH20);
3437 account_a_bytecode.extend_from_slice(account_b.as_slice());
3438 account_a_bytecode.extend_from_slice(&bytes!("620f4240f15000"));
3439
3440 let account_b_bytecode = Bytecode::new_raw(bytes!("600160005500"));
3442
3443 let mut evm = create_funded_evm_t7(caller);
3444 evm.ctx.db_mut().insert_account_info(
3445 account_a,
3446 AccountInfo {
3447 code: Some(Bytecode::new_raw(account_a_bytecode.into())),
3448 ..Default::default()
3449 },
3450 );
3451 evm.ctx.db_mut().insert_account_info(
3452 account_b,
3453 AccountInfo {
3454 code: Some(account_b_bytecode),
3455 ..Default::default()
3456 },
3457 );
3458 seed_storage_credit_balance(&mut evm, account_a, 2);
3459 seed_storage_credit_balance(&mut evm, account_b, 0);
3460
3461 let tx = TxBuilder::new()
3462 .call(account_a, &[])
3463 .gas_limit(2_000_000)
3464 .build();
3465 let signed_tx = key_pair.sign_tx(tx)?;
3466 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
3467 let result = evm.transact_commit(tx_env)?;
3468 assert!(result.is_success(), "multi-account tx should succeed");
3469
3470 assert_eq!(
3471 storage_credit_balance(&evm, account_a),
3472 1,
3473 "A consumes its own storage credits"
3474 );
3475 assert_eq!(
3476 storage_credit_balance(&evm, account_b),
3477 0,
3478 "B cannot consume A's extra storage credits"
3479 );
3480
3481 Ok(())
3482 }
3483
3484 #[test]
3489 fn test_tip1060_same_tx_create_before_delete_different_slots() -> eyre::Result<()> {
3490 let key_pair = P256KeyPair::random();
3491 let caller = key_pair.address;
3492 let contract = Address::repeat_byte(0x64);
3493
3494 let bytecode = Bytecode::new_raw(bytes!("6001600055600060015500"));
3496
3497 let mut evm = create_funded_evm_t7(caller);
3498 evm.ctx.db_mut().insert_account_info(
3499 contract,
3500 AccountInfo {
3501 code: Some(bytecode),
3502 ..Default::default()
3503 },
3504 );
3505 evm.ctx
3506 .db_mut()
3507 .insert_account_storage(contract, U256::from(1), U256::ONE)?;
3508 seed_storage_credit_balance(&mut evm, contract, 0);
3509
3510 let tx = TxBuilder::new()
3511 .call(contract, &[])
3512 .gas_limit(1_000_000)
3513 .build();
3514 let signed_tx = key_pair.sign_tx(tx)?;
3515 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
3516 let result = evm.transact_commit(tx_env)?;
3517 assert!(
3518 result.is_success(),
3519 "create-before-delete tx should succeed"
3520 );
3521
3522 assert_eq!(storage_credit_balance(&evm, contract), 0);
3523 assert_eq!(
3524 result.tx_gas_used(),
3525 295_868,
3526 "one 245k deferred storage credit is applied"
3527 );
3528
3529 Ok(())
3530 }
3531
3532 #[test]
3537 fn test_tip1060_direct_storage_credits_no_end_of_tx_double_benefit() -> eyre::Result<()> {
3538 let key_pair = P256KeyPair::random();
3539 let caller = key_pair.address;
3540 let direct_contract = Address::repeat_byte(0x65);
3541 let refund_contract = Address::repeat_byte(0x66);
3542
3543 let create_body = bytes!("600160005500");
3545
3546 let mut evm = create_funded_evm_t7(caller);
3547 evm.ctx.db_mut().insert_account_info(
3548 direct_contract,
3549 AccountInfo {
3550 code: Some(bytecode_with_tip1060_mode(CreditMode::Direct, &create_body)),
3551 ..Default::default()
3552 },
3553 );
3554 evm.ctx.db_mut().insert_account_info(
3555 refund_contract,
3556 AccountInfo {
3557 code: Some(bytecode_with_tip1060_mode(CreditMode::Refund, &create_body)),
3558 ..Default::default()
3559 },
3560 );
3561 seed_storage_credit_balance(&mut evm, direct_contract, 2);
3563 seed_storage_credit_balance(&mut evm, refund_contract, 1);
3564
3565 let direct_tx = TxBuilder::new()
3566 .call(direct_contract, &[])
3567 .nonce(0)
3568 .gas_limit(1_000_000)
3569 .build();
3570 let signed_direct = key_pair.sign_tx(direct_tx)?;
3571 let direct = evm.transact_commit(TempoTxEnv::from_recovered_tx(&signed_direct, caller))?;
3572 assert!(direct.is_success());
3573 assert_eq!(
3574 storage_credit_balance(&evm, direct_contract),
3575 1,
3576 "Direct must not consume the surplus storage credit at settlement"
3577 );
3578
3579 let refund_tx = TxBuilder::new()
3580 .call(refund_contract, &[])
3581 .nonce(1)
3582 .gas_limit(1_000_000)
3583 .build();
3584 let signed_refund = key_pair.sign_tx(refund_tx)?;
3585 let refund = evm.transact_commit(TempoTxEnv::from_recovered_tx(&signed_refund, caller))?;
3586 assert!(refund.is_success());
3587 assert_eq!(storage_credit_balance(&evm, refund_contract), 0);
3588
3589 assert_eq!(
3590 direct.tx_gas_used(),
3591 293_927,
3592 "Direct gets the synchronous discount without an additional settlement refund"
3593 );
3594 assert_eq!(
3595 refund.tx_gas_used(),
3596 37_962,
3597 "Refund applies the deferred 245k settlement refund for comparison"
3598 );
3599
3600 Ok(())
3601 }
3602
3603 #[test]
3608 fn test_tip1060_sstore_clear_mint_saturates_at_u64_max() -> eyre::Result<()> {
3609 let contract = Address::repeat_byte(0x67);
3610
3611 let clear_bytecode = bytes!("600060005500");
3613
3614 let (_, evm) = run_tx_on_tip1060_contract_with_setup(
3615 CreditMode::Refund,
3616 contract,
3617 &clear_bytecode,
3618 |evm, contract| {
3619 evm.ctx
3620 .db_mut()
3621 .insert_account_storage(contract, U256::ZERO, U256::ONE)?;
3622 seed_storage_credit_balance(evm, contract, u64::MAX);
3623 Ok(())
3624 },
3625 )?;
3626 assert_eq!(storage_credit_balance(&evm, contract), u64::MAX);
3627
3628 Ok(())
3629 }
3630
3631 #[test]
3633 fn test_expiring_nonce_indexed_path_does_not_settle_storage_credits() -> eyre::Result<()> {
3634 use tempo_primitives::transaction::TEMPO_EXPIRING_NONCE_KEY;
3635
3636 let key_pair = P256KeyPair::random();
3637 let caller = key_pair.address;
3638 let timestamp = 1000u64;
3639 let valid_before = timestamp + 30;
3640
3641 let tx = TxBuilder::new()
3642 .call_identity(&[])
3643 .nonce_key(TEMPO_EXPIRING_NONCE_KEY)
3644 .valid_before(Some(valid_before))
3645 .gas_limit(500_000)
3646 .build();
3647 let signed_tx = key_pair.sign_tx(tx)?;
3648 let unindexed_tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
3649
3650 let mut indexed_tx_env = unindexed_tx_env.clone();
3651 indexed_tx_env
3652 .tempo_tx_env
3653 .as_mut()
3654 .expect("expiring nonce tx must be AA")
3655 .expiring_nonce_idx = Some(1);
3656
3657 let mut unindexed_evm = create_funded_evm_t7_with_timestamp(caller, timestamp);
3658 let unindexed_result = unindexed_evm.transact_commit(unindexed_tx_env)?;
3659 assert!(
3660 unindexed_result.is_success(),
3661 "unindexed expiring nonce tx should succeed"
3662 );
3663
3664 let mut indexed_evm = create_funded_evm_t7_with_timestamp(caller, timestamp);
3665 let indexed_result = indexed_evm.transact_commit(indexed_tx_env)?;
3666 assert!(
3667 indexed_result.is_success(),
3668 "indexed expiring nonce tx should succeed"
3669 );
3670
3671 assert_eq!(
3672 indexed_result.tx_gas_used(),
3673 unindexed_result.tx_gas_used(),
3674 "pointer restore must not create a TIP-1060 settlement discount"
3675 );
3676 assert_eq!(
3677 storage_credit_balance(&indexed_evm, NONCE_PRECOMPILE_ADDRESS),
3678 0,
3679 "expiring nonce bookkeeping must not accrue storage credits"
3680 );
3681
3682 Ok(())
3683 }
3684
3685 #[test]
3687 fn test_2d_nonce_preexecution_does_not_settle_nonce_storage_credits() -> eyre::Result<()> {
3688 let key_pair = P256KeyPair::random();
3689 let caller = key_pair.address;
3690 let nonce_key = U256::from(42_u64);
3691
3692 let tx = TxBuilder::new()
3693 .call_identity(&[])
3694 .nonce_key(nonce_key)
3695 .gas_limit(1_000_000)
3696 .build();
3697 let signed_tx = key_pair.sign_tx(tx)?;
3698 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
3699
3700 let mut baseline_evm = create_funded_evm_t7(caller);
3701 let baseline_result = baseline_evm.transact_commit(tx_env.clone())?;
3702 assert!(
3703 baseline_result.is_success(),
3704 "baseline 2D nonce tx should succeed"
3705 );
3706
3707 let mut credited_evm = create_funded_evm_t7(caller);
3708 seed_storage_credit_balance(&mut credited_evm, NONCE_PRECOMPILE_ADDRESS, 1);
3709 let credited_result = credited_evm.transact_commit(tx_env)?;
3710 assert!(
3711 credited_result.is_success(),
3712 "preseeded-credit 2D nonce tx should succeed"
3713 );
3714
3715 assert_eq!(
3716 credited_result.tx_gas_used(),
3717 baseline_result.tx_gas_used(),
3718 "2D nonce bookkeeping must not consume nonce storage credits for a gas discount"
3719 );
3720 assert_eq!(
3721 storage_credit_balance(&credited_evm, NONCE_PRECOMPILE_ADDRESS),
3722 1,
3723 "2D nonce bookkeeping must not consume pre-existing nonce storage credits"
3724 );
3725
3726 Ok(())
3727 }
3728
3729 #[test]
3733 fn test_aa_tx_gas_create_contract() -> eyre::Result<()> {
3734 let key_pair = P256KeyPair::random();
3735 let caller = key_pair.address;
3736
3737 let mut evm = create_funded_evm_t1(caller);
3738
3739 let initcode = vec![0x60, 0x00, 0x60, 0x00, 0xF3];
3741
3742 let tx = TxBuilder::new()
3744 .create(&initcode)
3745 .gas_limit(1_000_000)
3746 .build();
3747
3748 let signed_tx = key_pair.sign_tx(tx)?;
3749 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
3750
3751 let result = evm.transact_commit(tx_env)?;
3752 assert!(result.is_success(), "CREATE transaction should succeed");
3753
3754 let gas_used = result.tx_gas_used();
3756 assert_eq!(gas_used, 778720, "T1 CREATE contract gas should be exact");
3757
3758 Ok(())
3759 }
3760
3761 #[test]
3764 fn test_t4_create_tx_charges_hash_cost() -> eyre::Result<()> {
3765 let key_pair = P256KeyPair::random();
3766 let caller = key_pair.address;
3767
3768 let tx = TxBuilder::new()
3770 .create(&hex!("6001600c60003960016000f300"))
3771 .gas_limit(1_000_000)
3772 .build();
3773 let signed_tx = key_pair.sign_tx(tx)?;
3774
3775 let run_create = |without_word_cost: bool| -> eyre::Result<u64> {
3776 let mut evm = create_funded_evm_t4(caller);
3777 if without_word_cost {
3778 evm.ctx.cfg.gas_params.override_gas(vec![(
3779 revm::context_interface::cfg::GasId::keccak256_per_word(),
3780 0,
3781 )]);
3782 }
3783
3784 let result = evm.transact_commit(TempoTxEnv::from_recovered_tx(&signed_tx, caller))?;
3785 assert!(
3786 result.is_success(),
3787 "T4 CREATE transaction should succeed with keccak256_per_word={without_word_cost:?}"
3788 );
3789 Ok(result.tx_gas_used())
3790 };
3791
3792 assert_eq!(
3793 run_create(false)? - run_create(true)?, tempo_gas_params(TempoHardfork::T4).keccak256_cost(1),
3795 "generic CREATE should add HASH_COST(L) on top of the non-hash baseline"
3796 );
3797 Ok(())
3798 }
3799
3800 #[test]
3804 fn test_aa_tx_gas_create_with_2d_nonce() -> eyre::Result<()> {
3805 let key_pair = P256KeyPair::random();
3806 let caller = key_pair.address;
3807
3808 let mut evm = create_funded_evm_t1(caller);
3809
3810 let initcode = vec![0x60, 0x00, 0x60, 0x00, 0xF3];
3812 let nonce_key_2d = U256::from(42);
3813
3814 let tx1 = TxBuilder::new()
3817 .create(&initcode)
3818 .nonce_key(nonce_key_2d)
3819 .gas_limit(2_000_000)
3820 .build();
3821
3822 assert_eq!(
3824 evm.ctx
3825 .db()
3826 .basic_ref(caller)
3827 .ok()
3828 .flatten()
3829 .map(|a| a.nonce)
3830 .unwrap_or(0),
3831 0,
3832 "Caller account nonce should be 0 before first tx"
3833 );
3834
3835 let signed_tx1 = key_pair.sign_tx(tx1)?;
3836 let tx_env1 = TempoTxEnv::from_recovered_tx(&signed_tx1, caller);
3837
3838 let result1 = evm.transact_commit(tx_env1)?;
3839 assert!(result1.is_success(), "CREATE with 2D nonce should succeed");
3840
3841 assert_eq!(
3843 result1.tx_gas_used(),
3844 1028720,
3845 "T1 CREATE with 2D nonce (caller.nonce=0) gas should be exact"
3846 );
3847
3848 let nonce_key_2d_2 = U256::from(43);
3853 let tx2 = TxBuilder::new()
3854 .create(&initcode)
3855 .nonce_key(nonce_key_2d_2)
3856 .nonce(0) .gas_limit(2_000_000)
3858 .build();
3859
3860 let signed_tx2 = key_pair.sign_tx(tx2)?;
3861 let tx_env2 = TempoTxEnv::from_recovered_tx(&signed_tx2, caller);
3862
3863 let result2 = evm.transact_commit(tx_env2)?;
3864 assert!(
3865 result2.is_success(),
3866 "Second CREATE with 2D nonce should succeed"
3867 );
3868
3869 assert_eq!(
3871 result2.tx_gas_used(),
3872 778720,
3873 "T1 CREATE with 2D nonce (caller.nonce=1) gas should be exact"
3874 );
3875
3876 let gas_difference = result1.tx_gas_used() - result2.tx_gas_used();
3878 assert_eq!(
3879 gas_difference, 250_000,
3880 "Gas difference should be exactly new_account_cost (250,000), got {gas_difference:?}",
3881 );
3882
3883 Ok(())
3884 }
3885
3886 #[test]
3889 fn test_aa_tx_gas_create_with_expiring_nonce() -> eyre::Result<()> {
3890 use tempo_primitives::transaction::TEMPO_EXPIRING_NONCE_KEY;
3891
3892 let key_pair = P256KeyPair::random();
3893 let caller = key_pair.address;
3894 let initcode = vec![0x60, 0x00, 0x60, 0x00, 0xF3]; let timestamp = 1000u64;
3896 let valid_before = timestamp + 30;
3897
3898 let mut evm1 = create_funded_evm_t1_with_timestamp(caller, timestamp);
3900 let tx1 = TxBuilder::new()
3901 .create(&initcode)
3902 .nonce_key(TEMPO_EXPIRING_NONCE_KEY)
3903 .valid_before(Some(valid_before))
3904 .gas_limit(2_000_000)
3905 .build();
3906 let result1 = evm1.transact_commit(TempoTxEnv::from_recovered_tx(
3907 &key_pair.sign_tx(tx1)?,
3908 caller,
3909 ))?;
3910 assert!(result1.is_success());
3911 let gas_nonce_zero = result1.tx_gas_used();
3912
3913 let mut evm2 = create_funded_evm_t1_with_timestamp(caller, timestamp);
3915 evm2.ctx.db_mut().insert_account_info(
3916 caller,
3917 AccountInfo {
3918 balance: U256::from(DEFAULT_BALANCE),
3919 nonce: 1,
3920 ..Default::default()
3921 },
3922 );
3923 let tx2 = TxBuilder::new()
3924 .create(&initcode)
3925 .nonce_key(TEMPO_EXPIRING_NONCE_KEY)
3926 .valid_before(Some(valid_before))
3927 .gas_limit(2_000_000)
3928 .build();
3929 let result2 = evm2.transact_commit(TempoTxEnv::from_recovered_tx(
3930 &key_pair.sign_tx(tx2)?,
3931 caller,
3932 ))?;
3933 assert!(result2.is_success());
3934 let gas_nonce_one = result2.tx_gas_used();
3935
3936 assert_eq!(
3938 gas_nonce_zero - gas_nonce_one,
3939 250_000,
3940 "new_account_cost not charged"
3941 );
3942
3943 Ok(())
3944 }
3945
3946 #[test]
3949 fn test_aa_tx_gas_single_vs_multiple_calls() -> eyre::Result<()> {
3950 let key_pair = P256KeyPair::random();
3951 let caller = key_pair.address;
3952
3953 let mut evm1 = create_funded_evm_t1(caller);
3956 let tx1 = TxBuilder::new()
3957 .call_identity(&[0x01, 0x02, 0x03, 0x04])
3958 .gas_limit(500_000)
3959 .build();
3960
3961 let signed_tx1 = key_pair.sign_tx(tx1)?;
3962 let tx_env1 = TempoTxEnv::from_recovered_tx(&signed_tx1, caller);
3963 let result1 = evm1.transact_commit(tx_env1)?;
3964 assert!(result1.is_success());
3965 let gas_single = result1.tx_gas_used();
3966
3967 let mut evm2 = create_funded_evm_t1(caller);
3970 let tx2 = TxBuilder::new()
3971 .call_identity(&[0x01, 0x02, 0x03, 0x04])
3972 .call_identity(&[0x05, 0x06, 0x07, 0x08])
3973 .call_identity(&[0x09, 0x0A, 0x0B, 0x0C])
3974 .gas_limit(500_000)
3975 .build();
3976
3977 let signed_tx2 = key_pair.sign_tx(tx2)?;
3978 let tx_env2 = TempoTxEnv::from_recovered_tx(&signed_tx2, caller);
3979 let result2 = evm2.transact_commit(tx_env2)?;
3980 assert!(result2.is_success());
3981 let gas_triple = result2.tx_gas_used();
3982
3983 assert_eq!(gas_single, 278738, "T1 single call gas should be exact");
3985 assert_eq!(gas_triple, 284102, "T1 triple call gas should be exact");
3986 assert!(
3987 gas_triple > gas_single,
3988 "3 calls should cost more than 1 call"
3989 );
3990 assert!(
3991 gas_triple < gas_single * 3,
3992 "3 calls should cost less than 3x single call (base costs shared)"
3993 );
3994
3995 Ok(())
3996 }
3997
3998 #[test]
4001 fn test_aa_tx_gas_sload_cold_vs_warm() -> eyre::Result<()> {
4002 let key_pair = P256KeyPair::random();
4003 let caller = key_pair.address;
4004 let contract = Address::repeat_byte(0x58);
4005
4006 let mut evm = create_funded_evm_t1(caller);
4007
4008 let sload_bytecode = Bytecode::new_raw(bytes!("6000545060005450"));
4013 evm.ctx.db_mut().insert_account_info(
4014 contract,
4015 AccountInfo {
4016 code: Some(sload_bytecode),
4017 ..Default::default()
4018 },
4019 );
4020
4021 evm.ctx
4023 .db_mut()
4024 .insert_account_storage(contract, U256::ZERO, U256::from(0x1234))
4025 .unwrap();
4026
4027 let tx = TxBuilder::new()
4029 .call(contract, &[])
4030 .gas_limit(500_000)
4031 .build();
4032
4033 let signed_tx = key_pair.sign_tx(tx)?;
4034 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
4035
4036 let result = evm.transact_commit(tx_env)?;
4037 assert!(result.is_success(), "SLOAD transaction should succeed");
4038
4039 let gas_used = result.tx_gas_used();
4041 assert_eq!(gas_used, 280866, "T1 SLOAD cold/warm gas should be exact");
4042
4043 Ok(())
4044 }
4045
4046 #[test]
4051 fn test_system_call_and_inspector() -> eyre::Result<()> {
4052 let caller = Address::repeat_byte(0x01);
4053 let contract = Address::repeat_byte(0x42);
4054
4055 let bytecode = Bytecode::new_raw(bytes!("444360006000F3"));
4058
4059 {
4061 let mut evm = create_evm();
4062 evm.ctx.db_mut().insert_account_info(
4063 contract,
4064 AccountInfo {
4065 code: Some(bytecode.clone()),
4066 ..Default::default()
4067 },
4068 );
4069
4070 let result = evm.system_call_one_with_caller(caller, contract, Bytes::new())?;
4071 assert!(result.is_success());
4072 }
4073
4074 {
4076 let mut evm = create_evm_with_inspector(CountInspector::new());
4077 evm.ctx.db_mut().insert_account_info(
4078 contract,
4079 AccountInfo {
4080 code: Some(bytecode),
4081 ..Default::default()
4082 },
4083 );
4084
4085 let result = evm.inspect_one_system_call_with_caller(caller, contract, Bytes::new())?;
4087 assert!(result.is_success());
4088
4089 assert!(evm.inspector.call_count() > 0,);
4091
4092 evm.set_inspector(CountInspector::new());
4094
4095 assert_eq!(evm.inspector.call_count(), 0,);
4097
4098 let result = evm.inspect_one_system_call_with_caller(caller, contract, Bytes::new())?;
4100 assert!(result.is_success());
4101 assert!(evm.inspector.call_count() > 0);
4102 }
4103
4104 Ok(())
4105 }
4106
4107 #[test]
4117 fn test_key_authorization_t1() -> eyre::Result<()> {
4118 use tempo_precompiles::account_keychain::AccountKeychain;
4119
4120 let key_pair = P256KeyPair::random();
4121 let caller = key_pair.address;
4122
4123 let mut evm = create_funded_evm_t1(caller);
4125
4126 let block = TempoBlockEnv::default();
4128 {
4129 let ctx = &mut evm.ctx;
4130 let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
4131 let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
4132
4133 StorageCtx::enter(&mut provider, || {
4134 TIP20Setup::path_usd(caller)
4135 .with_issuer(caller)
4136 .with_mint(caller, U256::from(10_000_000))
4137 .apply()
4138 })?;
4139 }
4140
4141 let access_key = P256KeyPair::random();
4145 let key_auth =
4146 KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, access_key.address);
4147 let key_auth_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
4148 let signed_key_auth = key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig));
4149
4150 {
4152 let ctx = &mut evm.ctx;
4153 let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
4154 let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
4155
4156 let key_exists = StorageCtx::enter(&mut provider, || {
4157 let keychain = AccountKeychain::default();
4158 keychain.keys[caller][access_key.address].read()
4159 })?;
4160 assert_eq!(
4161 key_exists.expiry, 0,
4162 "Key should not exist before transaction"
4163 );
4164 }
4165
4166 let signed_auth = key_pair.create_signed_authorization(Address::repeat_byte(0x42))?;
4167
4168 let tx_low_gas = TxBuilder::new()
4170 .call_identity(&[0x01])
4171 .authorization(signed_auth)
4172 .key_authorization(signed_key_auth)
4173 .gas_limit(589_000)
4174 .build();
4175
4176 let signed_tx_low = key_pair.sign_tx(tx_low_gas)?;
4177 let tx_env_low = TempoTxEnv::from_recovered_tx(&signed_tx_low, caller);
4178
4179 let result_low = evm.transact_commit(tx_env_low);
4181
4182 let nonce_incremented = match &result_low {
4185 Ok(result) => {
4186 assert_eq!(
4187 result.tx_gas_used(),
4188 589_000,
4189 "Gas used should be gas limit"
4190 );
4191 assert!(
4192 !result.is_success(),
4193 "Transaction with insufficient gas should fail"
4194 );
4195 true }
4197 Err(e) => {
4198 assert!(
4200 matches!(
4201 e,
4202 revm::context::result::EVMError::Transaction(
4203 TempoInvalidTransaction::EthInvalidTransaction(
4204 revm::context::result::InvalidTransaction::CallGasCostMoreThanGasLimit { .. }
4205 )
4206 )
4207 ),
4208 "Expected CallGasCostMoreThanGasLimit, got: {e:?}"
4209 );
4210 false }
4212 };
4213
4214 {
4217 let ctx = &mut evm.ctx;
4218 let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
4219 let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
4220
4221 let key_after_fail = StorageCtx::enter(&mut provider, || {
4222 let keychain = AccountKeychain::default();
4223 keychain.keys[caller][access_key.address].read()
4224 })?;
4225
4226 assert_eq!(
4227 key_after_fail,
4228 AuthorizedKey::default(),
4229 "Key should NOT be authorized when transaction fails due to insufficient gas"
4230 );
4231 }
4232
4233 let access_key2 = P256KeyPair::random();
4237 let key_auth2 =
4238 KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, access_key2.address);
4239 let key_auth_sig2 = key_pair.sign_webauthn(key_auth2.signature_hash().as_slice())?;
4240 let signed_key_auth2 = key_auth2.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig2));
4241
4242 let signed_auth2 = key_pair.create_signed_authorization(Address::repeat_byte(0x43))?;
4243
4244 let next_nonce = if nonce_incremented { 1 } else { 0 };
4246 let tx = TxBuilder::new()
4247 .call_identity(&[0x01])
4248 .authorization(signed_auth2)
4249 .key_authorization(signed_key_auth2)
4250 .nonce(next_nonce)
4251 .gas_limit(1_000_000)
4252 .build();
4253
4254 let signed_tx = key_pair.sign_tx(tx)?;
4255 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
4256
4257 let result = evm.transact_commit(tx_env)?;
4258 assert!(result.is_success(), "Transaction should succeed");
4259
4260 {
4262 let ctx = &mut evm.ctx;
4263 let internals = EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
4264 let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
4265
4266 let key_after_success = StorageCtx::enter(&mut provider, || {
4267 let keychain = AccountKeychain::default();
4268 keychain.keys[caller][access_key2.address].read()
4269 })?;
4270
4271 assert_eq!(
4272 key_after_success.expiry,
4273 u64::MAX,
4274 "Key should be authorized after successful transaction"
4275 );
4276 }
4277
4278 Ok(())
4279 }
4280
4281 #[test]
4296 fn test_create_nonce_replay_regression() -> eyre::Result<()> {
4297 use tempo_precompiles::account_keychain::AccountKeychain;
4298
4299 fn run_create_with_key_auth(
4302 spec: TempoHardfork,
4303 gas_limit: u64,
4304 ) -> eyre::Result<(u64, u64)> {
4305 let key_pair = P256KeyPair::random();
4306 let caller = key_pair.address;
4307
4308 let db = CacheDB::new(EmptyDB::new());
4309 let mut cfg = CfgEnv::<TempoHardfork>::default();
4310 cfg.spec = spec;
4311 cfg.gas_params = tempo_gas_params(spec);
4312
4313 let ctx = Context::mainnet()
4314 .with_db(db)
4315 .with_block(Default::default())
4316 .with_cfg(cfg)
4317 .with_tx(Default::default());
4318
4319 let mut evm = TempoEvm::new(ctx, ());
4320 fund_account(&mut evm, caller);
4321
4322 let block = TempoBlockEnv::default();
4323 {
4324 let ctx = &mut evm.ctx;
4325 let internals =
4326 EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
4327 let mut provider =
4331 EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
4332 StorageCtx::enter(&mut provider, || {
4333 TIP20Setup::path_usd(caller)
4334 .with_issuer(caller)
4335 .with_mint(caller, U256::from(100_000_000))
4336 .apply()
4337 })?;
4338 }
4339
4340 let access_key = P256KeyPair::random();
4341 let key_auth =
4342 KeyAuthorization::unrestricted(1, SignatureType::WebAuthn, access_key.address);
4343 let key_auth_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
4344 let signed_key_auth = key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig));
4345
4346 let tx = TxBuilder::new()
4347 .create(&[0x60, 0x00, 0x60, 0x00, 0xF3])
4348 .key_authorization(signed_key_auth)
4349 .gas_limit(gas_limit)
4350 .build();
4351
4352 let signed_tx = key_pair.sign_tx(tx)?;
4353 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
4354 let _result = evm.transact_commit(tx_env);
4355
4356 let nonce = evm
4357 .ctx
4358 .db()
4359 .basic_ref(caller)
4360 .ok()
4361 .flatten()
4362 .map(|a| a.nonce)
4363 .unwrap_or(0);
4364
4365 let key_expiry = {
4366 let ctx = &mut evm.ctx;
4367 let internals =
4368 EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
4369 let mut provider =
4370 EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
4371 let key = StorageCtx::enter(&mut provider, || {
4372 AccountKeychain::default().keys[caller][access_key.address].read()
4373 })?;
4374 key.expiry
4375 };
4376
4377 Ok((nonce, key_expiry))
4378 }
4379
4380 let (t1_nonce, t1_key_expiry) = run_create_with_key_auth(TempoHardfork::T1, 780_000)?;
4385 assert_eq!(
4386 t1_nonce, 0,
4387 "T1 bug: nonce must NOT be bumped when keychain OOGs"
4388 );
4389 assert_eq!(
4390 t1_key_expiry, 0,
4391 "T1 bug: key must NOT be authorized when keychain OOGs"
4392 );
4393
4394 let (t1b_nonce, t1b_key_expiry) = run_create_with_key_auth(TempoHardfork::T1B, 1_050_000)?;
4400 assert_eq!(
4401 t1b_nonce, 1,
4402 "T1B fix: nonce must be bumped after CREATE+KeyAuth"
4403 );
4404 assert_eq!(t1b_key_expiry, u64::MAX, "T1B fix: key must be authorized");
4405
4406 Ok(())
4407 }
4408
4409 #[test]
4420 fn test_double_charge_key_authorization_regression() -> eyre::Result<()> {
4421 fn run_call_with_key_auth(spec: TempoHardfork) -> eyre::Result<u64> {
4423 let key_pair = P256KeyPair::random();
4424 let caller = key_pair.address;
4425
4426 let db = CacheDB::new(EmptyDB::new());
4427 let mut cfg = CfgEnv::<TempoHardfork>::default();
4428 cfg.spec = spec;
4429 cfg.gas_params = tempo_gas_params(spec);
4430
4431 let ctx = Context::mainnet()
4432 .with_db(db)
4433 .with_block(Default::default())
4434 .with_cfg(cfg)
4435 .with_tx(Default::default());
4436
4437 let mut evm = TempoEvm::new(ctx, ());
4438 fund_account(&mut evm, caller);
4439
4440 let block = TempoBlockEnv::default();
4441 {
4442 let ctx = &mut evm.ctx;
4443 let internals =
4444 EvmInternals::new(&mut ctx.journaled_state, &block, &ctx.cfg, &ctx.tx);
4445 let mut provider =
4446 EvmPrecompileStorageProvider::new_max_gas(internals, &Default::default());
4447 StorageCtx::enter(&mut provider, || {
4448 TIP20Setup::path_usd(caller)
4449 .with_issuer(caller)
4450 .with_mint(caller, U256::from(100_000_000))
4451 .apply()
4452 })?;
4453 }
4454
4455 let access_key = P256KeyPair::random();
4456 let key_auth =
4457 KeyAuthorization::unrestricted(1, SignatureType::Secp256k1, access_key.address);
4458 let key_auth_sig = key_pair.sign_webauthn(key_auth.signature_hash().as_slice())?;
4459 let signed_key_auth = key_auth.into_signed(PrimitiveSignature::WebAuthn(key_auth_sig));
4460
4461 let tx = TxBuilder::new()
4462 .call_identity(&[])
4463 .key_authorization(signed_key_auth)
4464 .gas_limit(2_000_000)
4465 .build();
4466
4467 let signed_tx = key_pair.sign_tx(tx)?;
4468 let tx_env = TempoTxEnv::from_recovered_tx(&signed_tx, caller);
4469 let result = evm.transact_commit(tx_env)?;
4470 assert!(result.is_success());
4471 Ok(result.tx_gas_used())
4472 }
4473
4474 let t1_gas = run_call_with_key_auth(TempoHardfork::T1)?;
4475 let t1b_gas = run_call_with_key_auth(TempoHardfork::T1B)?;
4476
4477 assert!(
4480 t1_gas > 500_000,
4481 "T1 bug: should double-charge (got {t1_gas}, expected >500k)"
4482 );
4483
4484 assert!(
4488 t1b_gas < t1_gas,
4489 "T1B fix: gas ({t1b_gas}) must be less than T1 double-charge ({t1_gas})"
4490 );
4491
4492 Ok(())
4493 }
4494
4495 #[test]
4507 fn test_aa_tx_transfer_calls_format_no_extra_250k() -> eyre::Result<()> {
4508 let key_pair = P256KeyPair::random();
4509 let caller = key_pair.address;
4510 let recipient = Address::with_last_byte(0xff);
4511
4512 let mut evm_baseline = create_funded_evm_t1(caller);
4516 let tx_baseline = TxBuilder::new()
4517 .call(recipient, &[])
4518 .nonce_key(U256::ZERO)
4519 .nonce(0)
4520 .gas_limit(500_000)
4521 .build();
4522 let result_baseline = evm_baseline.transact_commit(TempoTxEnv::from_recovered_tx(
4523 &key_pair.sign_tx(tx_baseline)?,
4524 caller,
4525 ))?;
4526 assert!(
4527 result_baseline.is_success(),
4528 "baseline transfer should succeed"
4529 );
4530 let gas_baseline = result_baseline.tx_gas_used();
4531
4532 let nonce_key = U256::from(42);
4537 let mut evm_2d = create_funded_evm_t1(caller);
4538 let tx_2d = TxBuilder::new()
4539 .call(recipient, &[])
4540 .nonce_key(nonce_key)
4541 .nonce(0)
4542 .gas_limit(500_000)
4543 .build();
4544 let result_2d = evm_2d.transact_commit(TempoTxEnv::from_recovered_tx(
4545 &key_pair.sign_tx(tx_2d)?,
4546 caller,
4547 ))?;
4548 assert!(
4549 result_2d.is_success(),
4550 "calls-format transfer with 2D nonce should succeed"
4551 );
4552 let gas_2d = result_2d.tx_gas_used();
4553
4554 let diff = gas_2d.saturating_sub(gas_baseline);
4559 assert!(
4560 diff < 10_000,
4561 "calls-format transfer with nonceKey={nonce_key} (gas={gas_2d}) must not cost \
4562 ~250k more than baseline (gas={gas_baseline}, diff={diff}). \
4563 A diff near 250_000 means new_account_cost is incorrectly added for \
4564 transfers (issue #3178)."
4565 );
4566
4567 Ok(())
4568 }
4569}