Skip to content

feat: design multi signature scheme system#20

Open
tac0turtle wants to merge 1 commit intomainfrom
issue-18-sender-type-payload
Open

feat: design multi signature scheme system#20
tac0turtle wants to merge 1 commit intomainfrom
issue-18-sender-type-payload

Conversation

@tac0turtle
Copy link
Contributor

@tac0turtle tac0turtle commented Feb 28, 2026

Overview

Design and implement a multi signature scheme system that allows users to migrate verification schemes of their accounts and chains can add thier own custom schemes

Summary by CodeRabbit

  • New Features

    • Support for custom sender authentication types beyond standard Ethereum EOA
    • Dynamic registration of signature verifiers for custom sender types
    • Enhanced transaction context handling for multiple payload formats
  • Refactor

    • Restructured transaction verification to dispatch by sender type instead of transaction type
    • Improved modularity of signature verification system
  • Chores

    • Added test application with E2E testing infrastructure
    • Optimized container build caching for toolchain layer

@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

These changes introduce sender-type-based signature verification, wire-encoded transaction formats, and a post-bootstrap transaction hook. The verifier system shifts from tx-type dispatch to sender-type dispatch to support custom authentication mechanisms like Ed25519, enabling non-EOA senders alongside Ethereum secp256k1 EOAs.

Changes

Cohort / File(s) Summary
Transaction Authentication & Verification Framework
crates/app/tx/eth/src/payload.rs, crates/app/tx/eth/src/sender_type.rs, crates/app/tx/eth/src/verifier/registry.rs, crates/app/tx/eth/src/verifier/mod.rs, crates/app/tx/eth/src/gateway.rs
Introduced TxPayload enum with EOA and Custom variants; added sender-type constants (EOA_SECP256K1, CUSTOM); refactored SignatureVerifierRegistry to dispatch by sender-type instead of tx-type; added EoaPayloadVerifier adapter; implemented dynamic verifier registration and payload-based verification in gateway with register_payload_verifier(), supports_sender_type(), and verify_context() methods.
Memory Pool & Transaction Context
crates/app/tx/eth/src/mempool.rs, crates/app/tx/eth/src/lib.rs
Reworked TxContext from envelope-centric to payload-based model; added wire-encoded format support with TX_CONTEXT_WIRE_MAGIC and TxContextWireV1; introduced TxContextMeta to encapsulate metadata; added constructors from_payload(), from_eth_intent(), and is_wire_encoded(); expanded public accessors for hash, sender_type, sender/recipient addresses, and chain_id; updated public exports to include new payload and sender-type modules.
EOA Registry & Error Handling
crates/app/tx/eth/src/eoa_registry.rs, crates/app/tx/eth/src/error.rs
Made ETH_EOA_CODE_ID public; introduced ensure_eoa_mapping() for conflict-aware, idempotent address-to-account mapping with cross-consistency validation; added new error variant ERR_UNSUPPORTED_SENDER_TYPE with code 0x17.
Transaction Trait Lifecycle
crates/app/stf/src/lib.rs, crates/app/sdk/stf_traits/src/lib.rs
Added optional after_sender_bootstrap() hook to Transaction trait to enable post-bootstrap sender state initialization; provided default no-op implementation; invoked hook in STF after sender bootstrap registration.
E2E Testing — Ed25519 Authentication
bin/testapp/tests/mempool_e2e.rs, bin/testapp/Cargo.toml
Implemented Ed25519-based authentication flow with Ed25519AuthPayload, Ed25519EthIntentProof, Ed25519PayloadVerifier, and Ed25519AuthAccount; added storage helpers init_ed25519_auth_storage() and set_token_balance(); created test setup setup_genesis_with_ed25519_sender(), transaction builders build_ed25519_custom_tx_context(), and E2E test test_custom_sender_ed25519_transfer_e2e() to verify nonce and balance changes; added ed25519-consensus dev dependency.
Infrastructure & Runtime
crates/rpc/chain-index/src/provider.rs, crates/rpc/evnode/src/service.rs, docker-compose.testapp.yml, docker/evd/Dockerfile
Added with_mempool_and_gateway() constructor to ChainStateProvider for explicit gateway injection and test support of custom wire payloads; updated gas access from envelope().gas_limit() to gas_limit(); introduced testapp docker-compose service with gRPC and RPC ports; improved Dockerfile caching by installing Rust toolchain before copying source.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Gateway as EthGateway
    participant Registry as VerifierRegistry
    participant Verifier as SignatureVerifierDyn
    participant Mempool
    
    Client->>Gateway: send raw transaction (wire-encoded)
    
    alt EOA Envelope Format
        Gateway->>Gateway: decode as TxEnvelope
        Gateway->>Registry: verify_payload(EOA_SECP256K1, TxPayload::Eoa(...))
    else Custom Wire Format
        Gateway->>Gateway: detect wire magic, decode TxContext
        Gateway->>Gateway: extract TxPayload::Custom
        Gateway->>Registry: verify_payload(sender_type, TxPayload::Custom(...))
    end
    
    Registry->>Verifier: verify(&payload)
    alt Valid Signature
        Verifier-->>Registry: Ok(())
        Registry-->>Gateway: Ok(verified_context)
    else Invalid/Unsupported
        Verifier-->>Registry: Err(signature/sender_type error)
        Registry-->>Gateway: Err(GatewayError)
        Gateway-->>Client: Reject
    end
    
    Gateway->>Mempool: submit verified TxContext
    Mempool-->>Client: success / mempool entry
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Hops with glee through sender types so fine,
Ed25519 keys and wire formats align,
From custom payloads to hooks anew,
Ethereum evolves—more auth paths break through! 🔐✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: design multi signature scheme system' accurately reflects the core objective of implementing a multi-signature scheme system with custom verifiers and sender-type dispatch, which is evident throughout the substantial changes to the verifier architecture, payload handling, and authentication flows.
Docstring Coverage ✅ Passed Docstring coverage is 87.37% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-18-sender-type-payload

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@tac0turtle tac0turtle marked this pull request as ready for review February 28, 2026 16:53
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (13)
docker-compose.testapp.yml (1)

2-26: Consider adding a health check and restart policy.

For improved reliability during development and CI testing, you may want to add:

    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8545"]
      interval: 10s
      timeout: 5s
      retries: 3
    restart: unless-stopped

This helps with container orchestration and ensures the service recovers from transient failures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose.testapp.yml` around lines 2 - 26, Add a container healthcheck
and restart policy to the testapp service in docker-compose.testapp.yml: under
the service named "testapp" add a healthcheck that uses a CMD curl -f against
http://localhost:8545 with interval 10s, timeout 5s and retries 3, and set
restart to "unless-stopped" so the container is automatically restarted on
transient failures; ensure these keys are placed alongside existing "ports",
"environment", and "volumes" entries for the testapp service.
bin/testapp/tests/mempool_e2e.rs (4)

577-581: Hardcoded nonce limits test reusability.

The nonce is hardcoded to 0 in Ed25519AuthPayload. While this works for the single-transaction test, consider parameterizing it in Ed25519CustomTxBuildInput for future multi-transaction test scenarios.

Suggested enhancement
 struct Ed25519CustomTxBuildInput<'a> {
     tx_template_signer: &'a SigningKey,
     auth_signer: &'a Ed25519SigningKey,
     chain_id: u64,
     token_address: Address,
     recipient_account_id: AccountId,
     transfer_amount: u128,
     sender_account_id: AccountId,
+    nonce: u64,
 }

Then use input.nonce instead of the literal 0.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/testapp/tests/mempool_e2e.rs` around lines 577 - 581, Ed25519AuthPayload
is being constructed with a hardcoded nonce (0); change the code to take the
nonce from the transaction builder input instead. Update the builder/input
struct (Ed25519CustomTxBuildInput) to include a nonce field if it doesn't exist,
pass that input through to the test, and replace the literal 0 with input.nonce
when constructing auth_payload in the test where Ed25519AuthPayload and
auth_payload are created so multi-transaction tests can supply different nonces.

802-806: Clarify the scope of the local verifier registry.

The SignatureVerifierRegistry created here is used only to verify the payload before submission, but it's not integrated into the actual block execution path (the STF). This is fine for testing the verifier logic in isolation, but consider adding a comment clarifying that the production path would need the registry configured in the STF/gateway.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/testapp/tests/mempool_e2e.rs` around lines 802 - 806, The local
SignatureVerifierRegistry created here (SignatureVerifierRegistry::new(),
registry.register_dyn(sender_types::CUSTOM, Ed25519PayloadVerifier), and the
call to registry.verify_payload(sender_types::CUSTOM, tx_context.payload())) is
only being used to test the verifier logic prior to submission and is not wired
into the STF/block execution path; add a concise comment above this block
stating that this registry is local to the test and that in production the
verifier registry must be configured and injected into the STF/gateway so the
block execution path uses the same verifiers.

91-105: Error code collision with Ed25519PayloadVerifier.

Error codes 0x71-0x74 defined here overlap with those used (or suggested) in Ed25519PayloadVerifier (lines 59-77). If both components are used in the same error path, distinguishing the failure source becomes difficult.

Consider using a separate range for account-level errors vs verifier-level errors, or documenting the error code allocation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/testapp/tests/mempool_e2e.rs` around lines 91 - 105, The error codes
0x71–0x74 in the functions err_invalid_auth_payload, err_nonce_mismatch,
err_invalid_public_key, and err_invalid_signature collide with those used by
Ed25519PayloadVerifier; change these account-level error functions to return
codes from a distinct, non-overlapping range (e.g., bump to 0x80–0x83 or define
an ACCOUNT_ERROR_BASE constant) and update any tests/comments accordingly so
verifier vs account errors are unambiguous, or alternatively add a clear
documented allocation comment that reserves 0x71–0x74 for verifier errors and
reassign account errors to a different range.

56-79: Consider using distinct error codes for different failure modes.

Lines 59, 62, 64, 65, 70, and 71 all return ErrorCode::new(0x75) for different error conditions (invalid payload type, Borsh decode failures, missing invoke request, encode failure). This makes debugging difficult when errors occur.

Suggested distinct error codes
 impl SignatureVerifierDyn for Ed25519PayloadVerifier {
     fn verify(&self, payload: &TxPayload) -> SdkResult<()> {
         let TxPayload::Custom(bytes) = payload else {
-            return Err(ErrorCode::new(0x75));
+            return Err(ErrorCode::new(0x70)); // Invalid payload type
         };
         let intent: EthIntentPayload =
-            borsh::from_slice(bytes).map_err(|_| ErrorCode::new(0x75))?;
+            borsh::from_slice(bytes).map_err(|_| ErrorCode::new(0x71))?; // Intent decode error
         let decoded: Ed25519EthIntentProof =
-            borsh::from_slice(&intent.auth_proof).map_err(|_| ErrorCode::new(0x75))?;
+            borsh::from_slice(&intent.auth_proof).map_err(|_| ErrorCode::new(0x72))?; // Proof decode error
         let envelope = intent.decode_envelope().map_err(|_| ErrorCode::new(0x75))?;
         let invoke_request = envelope
             .to_invoke_requests()
             .into_iter()
             .next()
-            .ok_or_else(|| ErrorCode::new(0x75))?;
-        let request_digest = keccak256(&invoke_request.encode().map_err(|_| ErrorCode::new(0x75))?);
+            .ok_or_else(|| ErrorCode::new(0x73))?; // No invoke request
+        let request_digest = keccak256(&invoke_request.encode().map_err(|_| ErrorCode::new(0x74))?);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/testapp/tests/mempool_e2e.rs` around lines 56 - 79, The verify
implementation in Ed25519PayloadVerifier (impl SignatureVerifierDyn for
Ed25519PayloadVerifier -> fn verify) collapses multiple failure modes into
ErrorCode::new(0x75), hindering debugging; update each failure site (the
TxPayload match arm for TxPayload::Custom, both borsh::from_slice calls for
EthIntentPayload and Ed25519EthIntentProof, intent.decode_envelope(),
to_invoke_requests().next() missing-case, and invoke_request.encode()) to return
distinct ErrorCode values (e.g., 0x75..0x7A) so callers can distinguish invalid
payload type, intent decode error, auth_proof decode error, envelope decode
error, missing invoke request, and encode error respectively; change the
Err(...) and map_err(...) invocations to use the new unique ErrorCode::new(...)
values while keeping existing shapes and mapping locations intact (match arm,
borsh::from_slice map_err, decode_envelope map_err, ok_or_else for next, encode
map_err).
docker/evd/Dockerfile (1)

9-12: The fallback rustup toolchain install without arguments may not work as intended.

When rustup show active-toolchain fails (toolchain not installed), the fallback rustup toolchain install without specifying a toolchain name won't install anything meaningful. The command requires either a toolchain name argument or should rely on rustup's automatic toolchain installation.

Consider using rustup show which triggers auto-install from rust-toolchain.toml:

Proposed fix
-RUN rustup show active-toolchain || rustup toolchain install
+RUN rustup show

Alternatively, if you want explicit control:

RUN rustup toolchain install "$(cat rust-toolchain.toml | grep channel | cut -d'"' -f2)"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker/evd/Dockerfile` around lines 9 - 12, The fallback RUN line using
"rustup show active-toolchain || rustup toolchain install" won't install a
specific toolchain; update it to trigger auto-install from rust-toolchain.toml
by replacing the fallback with a single "rustup show" (which consults
rust-toolchain.toml) or explicitly install the channel parsed from
rust-toolchain.toml; locate the COPY rust-toolchain.toml ./rust-toolchain.toml
and the RUN rustup show active-toolchain || rustup toolchain install lines and
either call rustup show alone or extract the "channel" from rust-toolchain.toml
and pass it to rustup toolchain install so a concrete toolchain is installed.
crates/app/tx/eth/src/eoa_registry.rs (1)

185-213: Potential inconsistent mapping state when forward mapping exists but reverse is missing.

In ensure_eoa_mapping, when the forward mapping (address -> account_id) exists and matches, but the reverse mapping (account_id -> address) does not exist (line 197 returns None), the function falls through to line 205 which checks only the reverse mapping. If the reverse doesn't exist, it creates both mappings via set_mapping.

However, at line 201, if the forward exists and matches, but the reverse also exists and matches, it returns Ok(()) early. This is correct.

The issue is at line 202: if lookup_address_in_env returns None (reverse doesn't exist), the code exits the outer if block and falls to line 205. This appears to be intentional fall-through behavior, but the nested structure makes it harder to follow.

Consider simplifying the logic for clarity:

♻️ Suggested clearer structure
 pub fn ensure_eoa_mapping(
     address: Address,
     account_id: AccountId,
     env: &mut dyn Environment,
 ) -> SdkResult<()> {
-    if let Some(existing) = lookup_account_id_in_env(address, env)? {
-        if existing != account_id {
-            return Err(ERR_ADDRESS_ACCOUNT_CONFLICT);
-        }
-        if let Some(existing_addr) = lookup_address_in_env(account_id, env)? {
-            if existing_addr != address {
-                return Err(ERR_ADDRESS_ACCOUNT_CONFLICT);
-            }
-            return Ok(());
-        }
-    }
-
-    if let Some(existing_addr) = lookup_address_in_env(account_id, env)? {
-        if existing_addr != address {
-            return Err(ERR_ADDRESS_ACCOUNT_CONFLICT);
-        }
-        return set_mapping(address, account_id, env);
-    }
-
-    set_mapping(address, account_id, env)
+    let forward = lookup_account_id_in_env(address, env)?;
+    let reverse = lookup_address_in_env(account_id, env)?;
+
+    // Validate consistency of any existing mappings
+    if let Some(existing_id) = forward {
+        if existing_id != account_id {
+            return Err(ERR_ADDRESS_ACCOUNT_CONFLICT);
+        }
+    }
+    if let Some(existing_addr) = reverse {
+        if existing_addr != address {
+            return Err(ERR_ADDRESS_ACCOUNT_CONFLICT);
+        }
+    }
+
+    // If both mappings already exist and are consistent, nothing to do
+    if forward.is_some() && reverse.is_some() {
+        return Ok(());
+    }
+
+    // Create or complete the mapping
+    set_mapping(address, account_id, env)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/app/tx/eth/src/eoa_registry.rs` around lines 185 - 213, The current
branching in ensure_eoa_mapping is confusing and can be simplified: first call
lookup_account_id_in_env(address) and if Some(existing) and existing !=
account_id return ERR_ADDRESS_ACCOUNT_CONFLICT; then call
lookup_address_in_env(account_id) and if Some(existing_addr) and existing_addr
!= address return ERR_ADDRESS_ACCOUNT_CONFLICT; if both lookups returned Some
and they match return Ok(()); otherwise call set_mapping(address, account_id,
env). Update ensure_eoa_mapping to use lookup_account_id_in_env,
lookup_address_in_env, ERR_ADDRESS_ACCOUNT_CONFLICT, and set_mapping in that
order to make the logic linear and avoid the nested fall-through.
crates/app/tx/eth/src/gateway.rs (2)

163-184: Error mapping may obscure the actual failure reason.

When verify_payload fails, the error is mapped by inspecting the chain ID post-hoc (lines 170-179). If the signature is invalid and the chain ID is wrong, this returns InvalidChainId rather than InvalidSignature. This could mislead debugging efforts.

Consider verifying chain ID before signature verification for clearer error semantics:

♻️ Suggested refactor
 pub fn verify_envelope(&self, envelope: TxEnvelope) -> Result<TxContext, GatewayError> {
+    // Validate chain ID first for clearer error reporting
+    match envelope.chain_id() {
+        Some(id) if id != self.chain_id => {
+            return Err(GatewayError::InvalidChainId {
+                expected: self.chain_id,
+                actual: Some(id),
+            });
+        }
+        None => {
+            return Err(GatewayError::InvalidChainId {
+                expected: self.chain_id,
+                actual: None,
+            });
+        }
+        _ => {}
+    }
+
     let payload = TxPayload::Eoa(Box::new(envelope.clone()));
 
-    // Verify chain ID and signature
     self.verifier
         .verify_payload(sender_type::EOA_SECP256K1, &payload)
-        .map_err(|_| match envelope.chain_id() {
-            Some(id) if id != self.chain_id => GatewayError::InvalidChainId {
-                expected: self.chain_id,
-                actual: Some(id),
-            },
-            None => GatewayError::InvalidChainId {
-                expected: self.chain_id,
-                actual: None,
-            },
-            _ => GatewayError::InvalidSignature,
-        })?;
+        .map_err(|_| GatewayError::InvalidSignature)?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/app/tx/eth/src/gateway.rs` around lines 163 - 184, In verify_envelope,
check the envelope chain ID against self.chain_id before calling
self.verifier.verify_payload so chain-id mismatches return
GatewayError::InvalidChainId deterministically; only if the chain ID matches (or
is absent if that’s acceptable) proceed to call verify_payload and map its
failure to GatewayError::InvalidSignature (or propagate the existing mapping for
None/Some mismatches handled earlier); keep the final TxContext::new(envelope,
self.base_fee).ok_or(GatewayError::ContractCreationNotSupported) unchanged.

187-218: Consider adding tests for new public API methods.

The new register_payload_verifier, supports_sender_type, and verify_context methods lack direct test coverage. While the underlying registry is tested separately, gateway-level integration tests would help ensure the wiring is correct.

Would you like me to generate test cases for the new gateway methods?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/app/tx/eth/src/gateway.rs` around lines 187 - 218, Add integration
tests in the existing tests module for EthGateway that exercise the new public
API: call register_payload_verifier on an EthGateway instance to register a mock
verifier and assert it is accepted (and errors handled), call
supports_sender_type with known supported/unsupported sender types and assert
the boolean result, and call verify_context with valid and invalid contexts to
assert Ok and Err(GatewayError::VerifyFailed(_)) outcomes; use EthGateway::new
or ::with_base_fee to construct, and reference the methods
register_payload_verifier, supports_sender_type, and verify_context so the tests
validate the gateway-level wiring rather than only the registry unit tests.
crates/app/tx/eth/src/mempool.rs (3)

498-501: Envelope decode uses base_fee=0, affecting effective_gas_price.

When decoding a raw envelope (non-wire format), base_fee=0 is used (line 500). For EIP-1559 transactions, this affects the calculated effective_gas_price. If this is intentional (e.g., for replay/recovery where ordering doesn't matter), consider adding a brief comment.

+        // Envelope-only decode path uses zero base fee; effective_gas_price
+        // is recalculated for mempool insertion in decode_and_verify.
         let envelope = TxEnvelope::decode(bytes)?;
         TxContext::new(envelope, 0).ok_or(ERR_RECIPIENT_REQUIRED)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/app/tx/eth/src/mempool.rs` around lines 498 - 501, The
TxEnvelope::decode + TxContext::new call currently constructs the TxContext with
base_fee=0 which changes effective_gas_price calculation for EIP-1559
transactions; update the code near TxEnvelope::decode / TxContext::new to either
compute/pass the correct base_fee derived from the envelope or (if zero is
intentional for replay/recovery) add a concise comment explaining why base_fee=0
is used and the implication for effective_gas_price and ordering; reference
TxEnvelope::decode, TxContext::new, effective_gas_price and
ERR_RECIPIENT_REQUIRED when making the change so reviewers can quickly find and
verify the intent.

330-336: recipient() returns AccountId::invalid() for None resolution.

When recipient_resolution is None, recipient() returns AccountId::invalid(). This sentinel value could propagate silently and cause subtle bugs if callers don't check for it.

Consider whether this should panic (like sender_address()) or whether AccountId::invalid() is a well-defined sentinel that callers are expected to handle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/app/tx/eth/src/mempool.rs` around lines 330 - 336, The recipient()
method currently returns AccountId::invalid() for RecipientResolution::None
which can silently propagate; change it to mirror sender_address() behavior and
fail loudly: replace the None branch to panic with a clear message (e.g. using
panic! or expect on an Option) instead of returning AccountId::invalid(), so
callers don't receive a silent sentinel; update the RecipientResolution::None
handling in recipient() (and ensure any tests/call sites reflect the new panic
semantics) while keeping the Account and EoaAddress branches using the existing
account and derive_eth_eoa_account_id(address) logic.

236-243: Panic-based accessors may cause runtime failures.

sender_address() (line 240) and envelope() (line 272) panic when called on incompatible payload types. While _opt variants exist, panics in library code can be surprising to callers.

Consider either:

  1. Documenting these as #[doc(hidden)] or internal-only
  2. Returning Result instead of panicking
  3. Adding debug assertions in tests to catch misuse early

The current approach is acceptable if callers are expected to check sender_type() or payload() first, but the panic messages should be more actionable.

Also applies to: 268-274

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/app/tx/eth/src/mempool.rs` around lines 236 - 243, The accessor
methods sender_address() and envelope() currently panic on incompatible
SenderResolution/Payload types which can cause runtime failures; change them to
return Result<T, Error> (e.g., Result<Address, MyError> / Result<EnvelopeType,
MyError>) instead of panicking so callers must handle the mismatch, update call
sites to propagate or handle the error, and add a clear, actionable error
variant/message indicating the expected variant and the actual variant;
alternatively, if these are truly internal-only, mark them #[doc(hidden)] and
replace the panic text with a more descriptive message and/or add debug_assert!
checks (but prefer the Result approach for public APIs).
crates/app/tx/eth/src/verifier/registry.rs (1)

90-96: Backward-compatible verify allocates unnecessarily.

The verify method creates a new Box allocation for every call via Box::new(tx.clone()). For high-throughput scenarios, consider whether callers can migrate to verify_payload directly.

This is acceptable for a transitional compatibility shim, but the allocation overhead should be documented or the method marked as deprecated if migration is expected.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/app/tx/eth/src/verifier/registry.rs` around lines 90 - 96, The
compatibility shim register::verify currently allocates by cloning the
TxEnvelope into a Box for every call (Box::new(tx.clone()) passed as
TxPayload::Eoa) — mark this method as deprecated and document the allocation
cost so callers migrate to verify_payload; specifically add a #[deprecated(...)]
attribute to the verify function, update its doc comment to state it performs a
heap allocation (via TxPayload::Eoa(Box::new(...))) and clearly recommend
callers call verify_payload(sender_type::EOA_SECP256K1, &TxPayload::Eoa(...))
directly to avoid the clone/Box allocation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/rpc/evnode/src/service.rs`:
- Line 13: The import list in service.rs includes an unused symbol MempoolTx
from evolve_mempool; remove MempoolTx from the use statement (leave Mempool and
SharedMempool) so the compiler warning goes away and imports only used symbols;
update the line containing "use evolve_mempool::{Mempool, MempoolTx,
SharedMempool};" to exclude MempoolTx.

---

Nitpick comments:
In `@bin/testapp/tests/mempool_e2e.rs`:
- Around line 577-581: Ed25519AuthPayload is being constructed with a hardcoded
nonce (0); change the code to take the nonce from the transaction builder input
instead. Update the builder/input struct (Ed25519CustomTxBuildInput) to include
a nonce field if it doesn't exist, pass that input through to the test, and
replace the literal 0 with input.nonce when constructing auth_payload in the
test where Ed25519AuthPayload and auth_payload are created so multi-transaction
tests can supply different nonces.
- Around line 802-806: The local SignatureVerifierRegistry created here
(SignatureVerifierRegistry::new(), registry.register_dyn(sender_types::CUSTOM,
Ed25519PayloadVerifier), and the call to
registry.verify_payload(sender_types::CUSTOM, tx_context.payload())) is only
being used to test the verifier logic prior to submission and is not wired into
the STF/block execution path; add a concise comment above this block stating
that this registry is local to the test and that in production the verifier
registry must be configured and injected into the STF/gateway so the block
execution path uses the same verifiers.
- Around line 91-105: The error codes 0x71–0x74 in the functions
err_invalid_auth_payload, err_nonce_mismatch, err_invalid_public_key, and
err_invalid_signature collide with those used by Ed25519PayloadVerifier; change
these account-level error functions to return codes from a distinct,
non-overlapping range (e.g., bump to 0x80–0x83 or define an ACCOUNT_ERROR_BASE
constant) and update any tests/comments accordingly so verifier vs account
errors are unambiguous, or alternatively add a clear documented allocation
comment that reserves 0x71–0x74 for verifier errors and reassign account errors
to a different range.
- Around line 56-79: The verify implementation in Ed25519PayloadVerifier (impl
SignatureVerifierDyn for Ed25519PayloadVerifier -> fn verify) collapses multiple
failure modes into ErrorCode::new(0x75), hindering debugging; update each
failure site (the TxPayload match arm for TxPayload::Custom, both
borsh::from_slice calls for EthIntentPayload and Ed25519EthIntentProof,
intent.decode_envelope(), to_invoke_requests().next() missing-case, and
invoke_request.encode()) to return distinct ErrorCode values (e.g., 0x75..0x7A)
so callers can distinguish invalid payload type, intent decode error, auth_proof
decode error, envelope decode error, missing invoke request, and encode error
respectively; change the Err(...) and map_err(...) invocations to use the new
unique ErrorCode::new(...) values while keeping existing shapes and mapping
locations intact (match arm, borsh::from_slice map_err, decode_envelope map_err,
ok_or_else for next, encode map_err).

In `@crates/app/tx/eth/src/eoa_registry.rs`:
- Around line 185-213: The current branching in ensure_eoa_mapping is confusing
and can be simplified: first call lookup_account_id_in_env(address) and if
Some(existing) and existing != account_id return ERR_ADDRESS_ACCOUNT_CONFLICT;
then call lookup_address_in_env(account_id) and if Some(existing_addr) and
existing_addr != address return ERR_ADDRESS_ACCOUNT_CONFLICT; if both lookups
returned Some and they match return Ok(()); otherwise call set_mapping(address,
account_id, env). Update ensure_eoa_mapping to use lookup_account_id_in_env,
lookup_address_in_env, ERR_ADDRESS_ACCOUNT_CONFLICT, and set_mapping in that
order to make the logic linear and avoid the nested fall-through.

In `@crates/app/tx/eth/src/gateway.rs`:
- Around line 163-184: In verify_envelope, check the envelope chain ID against
self.chain_id before calling self.verifier.verify_payload so chain-id mismatches
return GatewayError::InvalidChainId deterministically; only if the chain ID
matches (or is absent if that’s acceptable) proceed to call verify_payload and
map its failure to GatewayError::InvalidSignature (or propagate the existing
mapping for None/Some mismatches handled earlier); keep the final
TxContext::new(envelope,
self.base_fee).ok_or(GatewayError::ContractCreationNotSupported) unchanged.
- Around line 187-218: Add integration tests in the existing tests module for
EthGateway that exercise the new public API: call register_payload_verifier on
an EthGateway instance to register a mock verifier and assert it is accepted
(and errors handled), call supports_sender_type with known supported/unsupported
sender types and assert the boolean result, and call verify_context with valid
and invalid contexts to assert Ok and Err(GatewayError::VerifyFailed(_))
outcomes; use EthGateway::new or ::with_base_fee to construct, and reference the
methods register_payload_verifier, supports_sender_type, and verify_context so
the tests validate the gateway-level wiring rather than only the registry unit
tests.

In `@crates/app/tx/eth/src/mempool.rs`:
- Around line 498-501: The TxEnvelope::decode + TxContext::new call currently
constructs the TxContext with base_fee=0 which changes effective_gas_price
calculation for EIP-1559 transactions; update the code near TxEnvelope::decode /
TxContext::new to either compute/pass the correct base_fee derived from the
envelope or (if zero is intentional for replay/recovery) add a concise comment
explaining why base_fee=0 is used and the implication for effective_gas_price
and ordering; reference TxEnvelope::decode, TxContext::new, effective_gas_price
and ERR_RECIPIENT_REQUIRED when making the change so reviewers can quickly find
and verify the intent.
- Around line 330-336: The recipient() method currently returns
AccountId::invalid() for RecipientResolution::None which can silently propagate;
change it to mirror sender_address() behavior and fail loudly: replace the None
branch to panic with a clear message (e.g. using panic! or expect on an Option)
instead of returning AccountId::invalid(), so callers don't receive a silent
sentinel; update the RecipientResolution::None handling in recipient() (and
ensure any tests/call sites reflect the new panic semantics) while keeping the
Account and EoaAddress branches using the existing account and
derive_eth_eoa_account_id(address) logic.
- Around line 236-243: The accessor methods sender_address() and envelope()
currently panic on incompatible SenderResolution/Payload types which can cause
runtime failures; change them to return Result<T, Error> (e.g., Result<Address,
MyError> / Result<EnvelopeType, MyError>) instead of panicking so callers must
handle the mismatch, update call sites to propagate or handle the error, and add
a clear, actionable error variant/message indicating the expected variant and
the actual variant; alternatively, if these are truly internal-only, mark them
#[doc(hidden)] and replace the panic text with a more descriptive message and/or
add debug_assert! checks (but prefer the Result approach for public APIs).

In `@crates/app/tx/eth/src/verifier/registry.rs`:
- Around line 90-96: The compatibility shim register::verify currently allocates
by cloning the TxEnvelope into a Box for every call (Box::new(tx.clone()) passed
as TxPayload::Eoa) — mark this method as deprecated and document the allocation
cost so callers migrate to verify_payload; specifically add a #[deprecated(...)]
attribute to the verify function, update its doc comment to state it performs a
heap allocation (via TxPayload::Eoa(Box::new(...))) and clearly recommend
callers call verify_payload(sender_type::EOA_SECP256K1, &TxPayload::Eoa(...))
directly to avoid the clone/Box allocation.

In `@docker-compose.testapp.yml`:
- Around line 2-26: Add a container healthcheck and restart policy to the
testapp service in docker-compose.testapp.yml: under the service named "testapp"
add a healthcheck that uses a CMD curl -f against http://localhost:8545 with
interval 10s, timeout 5s and retries 3, and set restart to "unless-stopped" so
the container is automatically restarted on transient failures; ensure these
keys are placed alongside existing "ports", "environment", and "volumes" entries
for the testapp service.

In `@docker/evd/Dockerfile`:
- Around line 9-12: The fallback RUN line using "rustup show active-toolchain ||
rustup toolchain install" won't install a specific toolchain; update it to
trigger auto-install from rust-toolchain.toml by replacing the fallback with a
single "rustup show" (which consults rust-toolchain.toml) or explicitly install
the channel parsed from rust-toolchain.toml; locate the COPY rust-toolchain.toml
./rust-toolchain.toml and the RUN rustup show active-toolchain || rustup
toolchain install lines and either call rustup show alone or extract the
"channel" from rust-toolchain.toml and pass it to rustup toolchain install so a
concrete toolchain is installed.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 05639d7 and 11bb7b7.

📒 Files selected for processing (17)
  • bin/testapp/Cargo.toml
  • bin/testapp/tests/mempool_e2e.rs
  • crates/app/sdk/stf_traits/src/lib.rs
  • crates/app/stf/src/lib.rs
  • crates/app/tx/eth/src/eoa_registry.rs
  • crates/app/tx/eth/src/error.rs
  • crates/app/tx/eth/src/gateway.rs
  • crates/app/tx/eth/src/lib.rs
  • crates/app/tx/eth/src/mempool.rs
  • crates/app/tx/eth/src/payload.rs
  • crates/app/tx/eth/src/sender_type.rs
  • crates/app/tx/eth/src/verifier/mod.rs
  • crates/app/tx/eth/src/verifier/registry.rs
  • crates/rpc/chain-index/src/provider.rs
  • crates/rpc/evnode/src/service.rs
  • docker-compose.testapp.yml
  • docker/evd/Dockerfile

use evolve_core::encoding::{Decodable, Encodable};
use evolve_core::ReadonlyKV;
use evolve_mempool::{Mempool, SharedMempool};
use evolve_mempool::{Mempool, MempoolTx, SharedMempool};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n 'MempoolTx' crates/rpc/evnode/src/service.rs

Repository: evstack/ev-rs

Length of output: 116


Remove unused import MempoolTx.

MempoolTx is imported on line 13 but is not used anywhere in this file.

Fix
-use evolve_mempool::{Mempool, MempoolTx, SharedMempool};
+use evolve_mempool::{Mempool, SharedMempool};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
use evolve_mempool::{Mempool, MempoolTx, SharedMempool};
use evolve_mempool::{Mempool, SharedMempool};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/rpc/evnode/src/service.rs` at line 13, The import list in service.rs
includes an unused symbol MempoolTx from evolve_mempool; remove MempoolTx from
the use statement (leave Mempool and SharedMempool) so the compiler warning goes
away and imports only used symbols; update the line containing "use
evolve_mempool::{Mempool, MempoolTx, SharedMempool};" to exclude MempoolTx.

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.

1 participant