Skip to content

Load tests precompile updates#1659

Open
kmchicoine wants to merge 7 commits intomainfrom
kaley/precompile-load-tests
Open

Load tests precompile updates#1659
kmchicoine wants to merge 7 commits intomainfrom
kaley/precompile-load-tests

Conversation

@kmchicoine
Copy link
Contributor

Extends the base-load-tests framework with comprehensive precompile testing capabilities and improved configuration.

Precompile Changes:

  • Added PrecompileTarget enum with typed support for all 11 EVM precompiles (ecrecover, sha256, ripemd160, blake2f, bn254_add/mul/pairing, identity, modexp, kzg_point_evaluation)
  • Added PrecompileLooper Solidity contract for calling precompiles multiple times per transaction
  • Added iterations parameter to precompile config for batch testing scenarios (e.g., multi-signature verification)
  • Added repeat_count parameter to calldata transactions for testing compressible vs random data

Other changes also included:

  • Updated README
  • Removed "spam" terminology throughout, replaced with "transaction submission"

@kmchicoine kmchicoine requested review from meyer9 and refcell March 25, 2026 03:29
@cb-heimdall
Copy link
Collaborator

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

@vercel
Copy link

vercel bot commented Mar 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
base Ignored Ignored Mar 25, 2026 3:29am

Request Review

}

info!(total = %total_drained, "drain complete");
Ok(total_drained)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

total_drained is accumulated optimistically before transactions confirm, but some drain txs may fail to confirm (the timeout warning on line 748 logs and continues). This means the returned U256 overstates the actual amount drained when confirmations time out. Consider tracking confirmed vs unconfirmed amounts separately and returning only the confirmed total, or at minimum documenting that the return value is an upper bound.

let drain_gas_limit = 21_000u128;
// Use gas_limit * max_fee + buffer to ensure the tx value + gas cost never exceeds balance.
// The buffer covers any rounding in the node's cost validation.
let drain_gas_cost = U256::from(drain_gas_limit * max_fee + 10_000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This multiplication can overflow: drain_gas_limit (21_000 u128) * max_fee (which can be up to DEFAULT_MAX_GAS_PRICE = 1_000_000_000_000 u128) + 10_000. The product itself fits in u128, but then it's converted to U256::from(...). The arithmetic would be clearer and safer using U256 throughout:

Suggested change
let drain_gas_cost = U256::from(drain_gas_limit * max_fee + 10_000);
let drain_gas_cost = U256::from(drain_gas_limit) * U256::from(max_fee) + U256::from(10_000u64);


let precompile_addr = precompile_address(&self.id);

if self.iterations > 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When iterations > 1 but looper_contract is None, this silently falls through to the single direct-call path, sending only one precompile call. The config validation in test_config.rs catches iterations > 1 without looper_contract at parse time, but PrecompilePayload is a public type and with_options can be called directly with an inconsistent combination. Consider either:

  1. Validating in with_options (return Result or panic with a clear message), or
  2. Adding a debug_assert! here so misuse is caught in development.


/// Address of the precompile looper contract (required when using iterations > 1).
#[serde(default)]
pub looper_contract: Option<String>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looper_contract is stored as Option<String> and parsed to Address each time convert_tx_type is called (once per precompile entry in the transaction list). Since TestConfig is a deserialization type, this is fine for now, but consider using Option<Address> with a custom deserializer to make invalid states unrepresentable and avoid repeated parsing of the same string.

let selector = [0x7b, 0x39, 0x5f, 0xc2];

// ABI encode: address (32 bytes) + offset to bytes (32) + iterations (32) + bytes length (32) + bytes data (padded)
let data_len = data.len();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The data_len is cast to u32 here without a bounds check. While current precompile inputs are small (< 256 bytes), this is a latent truncation bug if larger inputs are ever used (e.g., large modexp inputs or identity copies). The encode_modexp_data function above can produce up to 96 + 32 + 32 + 32 = 192 bytes which is fine, but this is worth a note or a debug_assert!(data_len <= u32::MAX as usize).

Suggested change
let data_len = data.len();
let data_len: u32 = data.len().try_into().expect("precompile data must fit in u32");
let padded_data_len = (data_len as usize).div_ceil(32) * 32;

@github-actions
Copy link
Contributor

Review Summary

The PR adds precompile load testing with a well-designed PrecompileTarget enum, ABI encoding for a looper contract, account draining, and transaction replacement logic. The architecture is clean — typed config deserialization with #[serde(tag = "target")] is a good pattern. A few issues worth addressing:

Findings

  1. drain_accounts returns optimistic total (load_runner.rs:754) — total_drained is accumulated before confirmation. When drain txs time out, the returned value overstates the actual amount recovered. Should track confirmed vs unconfirmed separately.

  2. u128 arithmetic for gas cost (load_runner.rs:665) — The drain_gas_limit * max_fee + 10_000 computation works but would be clearer and more defensive using U256 throughout, since both operands can be large.

  3. Silent fallthrough when iterations > 1 without looper contract (precompile.rs:206) — The public with_options constructor allows inconsistent state (iterations > 1, looper_contract = None), which silently degrades to a single call. Config validation catches this at the YAML layer, but the public API doesn't enforce it.

  4. looper_contract stored as String (test_config.rs:116) — Parsed to Address on each convert_tx_type call. Using Option<Address> with a custom deserializer would make invalid states unrepresentable.

  5. Unchecked as u32 cast in ABI encoding (precompile.rs:151) — data.len() as u32 could truncate on very large inputs. A try_into() would be more defensive.

None of these are blocking, but items 1 and 3 are worth addressing before merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants