Skip to content

feat: design multi signature scheme system#20

Open
tac0turtle wants to merge 4 commits intomainfrom
issue-18-sender-type-payload
Open

feat: design multi signature scheme system#20
tac0turtle wants to merge 4 commits 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

    • Custom sender authentication support and dynamic registration of signature verifiers
    • Expanded transaction context to handle multiple payload formats (wire‑encoded custom payloads)
    • New post‑bootstrap transaction hook for additional sender setup
  • Refactor

    • Verification dispatch reorganized to route by sender type for greater flexibility
  • Bug Fixes / Improvements

    • Improved error reporting for unsupported sender types
  • Chores

    • New testapp service and end‑to‑end test infra (including ED25519 flows)
    • Docker build caching improvement for faster local builds

@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

Adds sender-type-based payload verification and a wire-encoded TxContext model, dynamic payload verifiers (including Ed25519 test verifier), and a Transaction post-bootstrap hook; updates gateway, verifier registry, mempool/TxContext, EOA mapping, tests, and integration wiring.

Changes

Cohort / File(s) Summary
Verification & Payload Types
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
Add TxPayload (Eoa/Custom) and sender type constants; refactor verifier registry to dispatch by sender_type (u16); introduce SignatureVerifierDyn, EoaPayloadVerifier adapter, dynamic registration APIs, and payload-based verification primitives.
Gateway & API Surface
crates/app/tx/eth/src/gateway.rs, crates/app/tx/eth/src/lib.rs
Extend EthGateway to register/query payload verifiers, decode/verify TxContext (including wire-encoded) and add error variants; re-export new modules/types.
Mempool & TxContext Model
crates/app/tx/eth/src/mempool.rs
Replace envelope-centric model with TxContext holding TxPayload, sender_type, metadata and a wire-encoded format; add TxContextMeta, constructors (from_payload, from_eth_intent), encodable/decodable wire format, resolution enums, and updated trait impls.
Verifier Registry Tests
crates/app/tx/eth/src/verifier/registry.rs (tests)
Adapt tests to sender_type-based checks and new registry API.
EOA Mapping & Errors
crates/app/tx/eth/src/eoa_registry.rs, crates/app/tx/eth/src/error.rs
Make ETH_EOA_CODE_ID public; add ensure_eoa_mapping() to centralize mapping creation/validation and conflict checks; add ERR_UNSUPPORTED_SENDER_TYPE error variant.
STF Transaction Hook
crates/app/sdk/stf_traits/src/lib.rs, crates/app/stf/src/lib.rs
Add after_sender_bootstrap(resolved_sender, env) hook to the Transaction trait (default no-op) and invoke it after sender bootstrap.
Ed25519 E2E Tests & Dev deps
bin/testapp/tests/mempool_e2e.rs, bin/testapp/Cargo.toml
Add Ed25519-based test scaffolding (auth payloads, verifier, storage helpers, custom TxContext builders) and add ed25519-consensus as a dev-dependency; new E2E test exercising CUSTOM sender flow.
Chain Index / RPC / Node Integration
crates/rpc/chain-index/src/provider.rs, crates/rpc/evnode/src/service.rs
Add with_mempool_and_gateway() to inject preconfigured EthGateway; update gas accessors to gas_limit(); add tests for custom wire payload verification.
Docker / Test App
docker-compose.testapp.yml, docker/evd/Dockerfile
Add testapp docker-compose service and adjust Dockerfile builder stage to install the toolchain early for layer caching.
Misc Tests & Helpers
bin/testapp/..., crates/... (tests)
Multiple test additions/updates to cover payload encode/decode, gateway/registry behaviors, custom sender flows, and E2E scenarios.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Gateway as EthGateway
    participant Registry as VerifierRegistry
    participant Verifier as SignatureVerifierDyn
    participant Mempool

    Client->>Gateway: submit raw tx (wire-encoded or envelope)
    alt Wire-encoded Custom
        Gateway->>Gateway: detect wire magic, decode TxContext -> TxPayload::Custom
        Gateway->>Registry: verify_payload(sender_type, TxPayload::Custom)
    else EOA Envelope
        Gateway->>Gateway: decode TxEnvelope -> TxPayload::Eoa
        Gateway->>Registry: verify_payload(EOA_SECP256K1, TxPayload::Eoa)
    end
    Registry->>Verifier: verify(&payload)
    alt Verified
        Verifier-->>Registry: Ok(())
        Registry-->>Gateway: Ok(verified)
        Gateway->>Mempool: submit verified TxContext
        Mempool-->>Client: accepted
    else Invalid / Unsupported
        Verifier-->>Registry: Err
        Registry-->>Gateway: Err(GatewayError)
        Gateway-->>Client: reject
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I nibble code and hop with cheer,

new sender types and verifiers appear,
Ed25519 whispers, wires hum along,
mempool drums a verification song,
Hooray — the chain hops lively and clear.

🚥 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 captures the main objective described in the PR description, which is designing and implementing a multi-signature scheme system with support for custom verification schemes.
Docstring Coverage ✅ Passed Docstring coverage is 85.29% 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

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 (4)
crates/app/tx/eth/src/mempool.rs (2)

268-274: Same concern: envelope() panics for custom payloads.

Similar to sender_address(), this accessor panics when called on Custom payloads. The envelope_opt() method provides a safe alternative. Consider consolidating to avoid accidental panics in production code.

🤖 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 268 - 274, The envelope()
accessor currently panics for TxPayload::Custom just like sender_address() —
replace or refactor it to avoid accidental panics by delegating to the safe
envelope_opt() or changing envelope() to return Option<&TxEnvelope>; update the
function body for envelope() (and any callers) to either return
envelope_opt().expect_* only at call sites that truly guarantee Eoa payloads, or
change envelope() to simply return envelope_opt() (Option) so callers must
handle the None case, referencing the envelope(), envelope_opt(), and
sender_address() symbols to locate and adjust the implementation and usages.

236-243: Consider returning Result instead of panicking.

The sender_address() method panics when called on non-EOA payloads. While the method documentation mentions this limitation, a panic in production code could be problematic. Consider either:

  1. Making this method return Option<Address> (similar to sender_address_opt)
  2. Removing the panicking method and keeping only sender_address_opt

Since sender_address_opt already exists, the panicking variant may be unnecessary.

♻️ Alternative: Remove panicking accessor

If callers always need to handle the None case anyway, consider deprecating or removing sender_address() in favor of sender_address_opt() to prevent accidental panics.

🤖 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 panicking
sender_address() should be made safe: change its signature to return
Option<Address> and implement it by delegating to the existing
sender_address_opt() (i.e., return self.sender_address_opt()), removing the
panic path; update any callers to handle the Option (or, if you prefer removal
instead, delete sender_address() and replace all usages with
sender_address_opt()). Ensure references to SenderResolution::EoaAddress /
SenderResolution::Account remain unchanged.
crates/app/tx/eth/src/eoa_registry.rs (2)

185-213: Add in-file tests for the repair and conflict branches.

This helper now owns the EOA mapping invariant, but the file still doesn't pin down the forward-only, reverse-only, and conflict cases. Please add unit tests next to this implementation.

As per coding guidelines, "Place unit tests alongside implementation in the same Rust file".

🤖 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, Add inline
unit tests in the same Rust file that exercise ensure_eoa_mapping for the
forward-only, reverse-only, successful-idempotent, repair (one side missing),
and conflict branches: create tests that set up a mock or test Environment, use
lookup_account_id_in_env and lookup_address_in_env to assert initial state, call
ensure_eoa_mapping with mismatched and matching pairs to trigger
ERR_ADDRESS_ACCOUNT_CONFLICT and successful mapping, and verify set_mapping was
called (or that both forward and reverse lookups return the expected
Address/AccountId) after repair; place tests immediately below the
ensure_eoa_mapping implementation and use the same helper functions
(lookup_account_id_in_env, lookup_address_in_env, set_mapping) so they run as
unit tests alongside the implementation.

188-213: Keep the registry invariant checks in one place.

ensure_eoa_mapping() repeats the same forward/reverse conflict checks that set_mapping() already does. That duplicates the invariant and adds extra reads before every repair. I’d keep the checks in one place and make the lower-level helper write-only.

🤖 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 188 - 213,
ensure_eoa_mapping duplicates forward/reverse conflict checks that set_mapping
already enforces, causing unnecessary reads; remove the
lookup_account_id_in_env/lookup_address_in_env calls and the duplicate conflict
logic from ensure_eoa_mapping and make it a write-only wrapper that simply calls
set_mapping(address, account_id, env) and returns its result, relying on
set_mapping to perform and return ERR_ADDRESS_ACCOUNT_CONFLICT when appropriate;
if set_mapping currently doesn't perform those checks, move the existing
conflict checks into set_mapping (using lookup_account_id_in_env and
lookup_address_in_env) and then simplify ensure_eoa_mapping to just call
set_mapping.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bin/testapp/tests/mempool_e2e.rs`:
- Around line 231-255: The formatting in init_ed25519_auth_storage (and the
preceding init_eth_auth_storage) is inconsistent with rustfmt; run `cargo fmt`
(or `just quality`) to reformat these functions so spacing/indentation and line
breaks match project style, ensuring expressions like let mut nonce_key =
account_id.as_bytes().to_vec(); nonce_key.push(0u8); data.insert(...); and the
pubkey_key insert lines are reformatted; verify the function signatures and
comment alignment for init_ed25519_auth_storage and the related data.insert
calls are corrected.

---

Nitpick comments:
In `@crates/app/tx/eth/src/eoa_registry.rs`:
- Around line 185-213: Add inline unit tests in the same Rust file that exercise
ensure_eoa_mapping for the forward-only, reverse-only, successful-idempotent,
repair (one side missing), and conflict branches: create tests that set up a
mock or test Environment, use lookup_account_id_in_env and lookup_address_in_env
to assert initial state, call ensure_eoa_mapping with mismatched and matching
pairs to trigger ERR_ADDRESS_ACCOUNT_CONFLICT and successful mapping, and verify
set_mapping was called (or that both forward and reverse lookups return the
expected Address/AccountId) after repair; place tests immediately below the
ensure_eoa_mapping implementation and use the same helper functions
(lookup_account_id_in_env, lookup_address_in_env, set_mapping) so they run as
unit tests alongside the implementation.
- Around line 188-213: ensure_eoa_mapping duplicates forward/reverse conflict
checks that set_mapping already enforces, causing unnecessary reads; remove the
lookup_account_id_in_env/lookup_address_in_env calls and the duplicate conflict
logic from ensure_eoa_mapping and make it a write-only wrapper that simply calls
set_mapping(address, account_id, env) and returns its result, relying on
set_mapping to perform and return ERR_ADDRESS_ACCOUNT_CONFLICT when appropriate;
if set_mapping currently doesn't perform those checks, move the existing
conflict checks into set_mapping (using lookup_account_id_in_env and
lookup_address_in_env) and then simplify ensure_eoa_mapping to just call
set_mapping.

In `@crates/app/tx/eth/src/mempool.rs`:
- Around line 268-274: The envelope() accessor currently panics for
TxPayload::Custom just like sender_address() — replace or refactor it to avoid
accidental panics by delegating to the safe envelope_opt() or changing
envelope() to return Option<&TxEnvelope>; update the function body for
envelope() (and any callers) to either return envelope_opt().expect_* only at
call sites that truly guarantee Eoa payloads, or change envelope() to simply
return envelope_opt() (Option) so callers must handle the None case, referencing
the envelope(), envelope_opt(), and sender_address() symbols to locate and
adjust the implementation and usages.
- Around line 236-243: The panicking sender_address() should be made safe:
change its signature to return Option<Address> and implement it by delegating to
the existing sender_address_opt() (i.e., return self.sender_address_opt()),
removing the panic path; update any callers to handle the Option (or, if you
prefer removal instead, delete sender_address() and replace all usages with
sender_address_opt()). Ensure references to SenderResolution::EoaAddress /
SenderResolution::Account remain unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a914c1b-6d6a-4e6a-8578-230e1e4e6df0

📥 Commits

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

📒 Files selected for processing (7)
  • bin/testapp/tests/mempool_e2e.rs
  • crates/app/tx/eth/src/eoa_registry.rs
  • crates/app/tx/eth/src/gateway.rs
  • crates/app/tx/eth/src/mempool.rs
  • crates/rpc/evnode/src/service.rs
  • docker-compose.testapp.yml
  • docker/evd/Dockerfile
🚧 Files skipped from review as they are similar to previous changes (1)
  • docker-compose.testapp.yml

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
bin/testapp/tests/mempool_e2e.rs (1)

607-654: ⚠️ Potential issue | 🟠 Major

This “E2E” case still bypasses the ingress wiring.

The custom-sender flow manually calls registry.verify_payload(...) and then pushes a prebuilt TxContext into the mempool. That means this test stays green even if the real gateway/provider registration for sender_types::CUSTOM breaks, which is one of the key integrations in this PR. Either route this through the production decode/verify entrypoint or rename/scope it as a post-verification execution test.

Also applies to: 798-817

🤖 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 607 - 654, The test helper
submit_context_and_produce_block currently bypasses the ingress wiring by
constructing and inserting a prebuilt TxContext (used by
submit_and_produce_block which calls
EthGateway::new(...).decode_and_verify(...)), so update the test to exercise the
real registration/verification path: replace the manual creation or direct
mempool.insert of TxContext with the production ingress decode/verify entrypoint
(i.e., call the registry/provider verify_payload flow or the same gateway
decode_and_verify used by real ingress) so sender_types::CUSTOM registration is
exercised; alternatively, if you intend this to be a post-verification unit
scenario, rename the helpers (submit_context_and_produce_block ->
submit_verified_context_and_produce_block) and add comments clarifying it does
not test registration/ingress, ensuring tests at lines ~798-817 get the same
treatment.
🧹 Nitpick comments (2)
bin/testapp/tests/mempool_e2e.rs (2)

57-79: Register these new error codes with define_error!.

This file adds several ad hoc ErrorCode::new(...) paths. The repo guideline requires define_error! so the codes are registered at compile time and their ranges stay explicit.

As per coding guidelines, "Define errors using the define_error! macro for compile-time error registration with error codes in ranges: 0x00-0x3F (validation), 0x40-0x7F (system), 0x80-0xBF (business logic)".

Also applies to: 92-106

🤖 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 57 - 79, The new
Ed25519PayloadVerifier::verify method currently uses ad-hoc ErrorCode::new(...)
calls (e.g., 0x75–0x7C) which must be declared with the define_error! macro; add
a define_error! block that registers named error variants for each used code
(matching the repo's ranges, e.g., system 0x40-0x7F) and then replace the inline
ErrorCode::new(...) usages in verify (and the other occurrences around lines
92-106) with the corresponding named error variants (or map to them via
ErrorCode::from or the defined identifiers) so all error codes are compile-time
registered and follow the project's error-range convention.

752-845: Split this test below the 70-line limit.

The setup, verifier bootstrap, submission, and state assertions are all in one block now. Pulling the initial-state reads and post-block assertions into helpers would keep the scenario easier to scan and align with the repo rule.

As per coding guidelines, "Keep functions to less than 70 lines to maintain bounded complexity".

🤖 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 752 - 845, The test
test_custom_sender_ed25519_transfer_e2e exceeds the 70-line guideline by
including setup, verifier bootstrap, submission, and assertions in one block;
refactor by extracting helper functions: create setup helper (e.g.,
setup_ed25519_sender_test) that wraps setup_genesis_with_ed25519_sender and
returns (handles, genesis_accounts, sender_account_id, bob_account_id,
auth_signer, alice_key, bob_key, token_address, sender_nonce_before,
sender_balance_before, bob_balance_before), an assertion helper (e.g.,
assert_post_transfer_state) that reads nonce/balances and performs the three
assert_eq! checks, and optionally a small helper for registry
bootstrap/verification (e.g., bootstrap_and_verify_custom_sender) to register
and verify payloads; replace the inline blocks in
test_custom_sender_ed25519_transfer_e2e with calls to these helpers so the test
body stays <70 lines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bin/testapp/tests/mempool_e2e.rs`:
- Around line 57-79: The verifier currently checks intent.auth_proof against the
invoke-request digest in Ed25519PayloadVerifier::verify but
Ed25519AuthAccount::authenticate trusts payload.message_digest from the caller,
allowing mismatched signatures; fix by making both sides use the same canonical
digest: compute the canonical preimage (e.g., nonce + request_digest + sender
binding) when verifying the intent in Ed25519PayloadVerifier::verify and either
(A) pass that computed digest into Ed25519AuthAccount::authenticate (do not
accept payload.message_digest from caller), or (B) have
Ed25519AuthAccount::authenticate recompute the digest internally from the
intent/envelope (not from authentication_payload) and verify the account
signature against that recomputed digest; ensure the unique symbols involved are
Ed25519PayloadVerifier::verify, intent.auth_proof, request_digest,
Ed25519AuthAccount::authenticate, payload.message_digest and
authentication_payload so both checks validate the same preimage.

---

Outside diff comments:
In `@bin/testapp/tests/mempool_e2e.rs`:
- Around line 607-654: The test helper submit_context_and_produce_block
currently bypasses the ingress wiring by constructing and inserting a prebuilt
TxContext (used by submit_and_produce_block which calls
EthGateway::new(...).decode_and_verify(...)), so update the test to exercise the
real registration/verification path: replace the manual creation or direct
mempool.insert of TxContext with the production ingress decode/verify entrypoint
(i.e., call the registry/provider verify_payload flow or the same gateway
decode_and_verify used by real ingress) so sender_types::CUSTOM registration is
exercised; alternatively, if you intend this to be a post-verification unit
scenario, rename the helpers (submit_context_and_produce_block ->
submit_verified_context_and_produce_block) and add comments clarifying it does
not test registration/ingress, ensuring tests at lines ~798-817 get the same
treatment.

---

Nitpick comments:
In `@bin/testapp/tests/mempool_e2e.rs`:
- Around line 57-79: The new Ed25519PayloadVerifier::verify method currently
uses ad-hoc ErrorCode::new(...) calls (e.g., 0x75–0x7C) which must be declared
with the define_error! macro; add a define_error! block that registers named
error variants for each used code (matching the repo's ranges, e.g., system
0x40-0x7F) and then replace the inline ErrorCode::new(...) usages in verify (and
the other occurrences around lines 92-106) with the corresponding named error
variants (or map to them via ErrorCode::from or the defined identifiers) so all
error codes are compile-time registered and follow the project's error-range
convention.
- Around line 752-845: The test test_custom_sender_ed25519_transfer_e2e exceeds
the 70-line guideline by including setup, verifier bootstrap, submission, and
assertions in one block; refactor by extracting helper functions: create setup
helper (e.g., setup_ed25519_sender_test) that wraps
setup_genesis_with_ed25519_sender and returns (handles, genesis_accounts,
sender_account_id, bob_account_id, auth_signer, alice_key, bob_key,
token_address, sender_nonce_before, sender_balance_before, bob_balance_before),
an assertion helper (e.g., assert_post_transfer_state) that reads nonce/balances
and performs the three assert_eq! checks, and optionally a small helper for
registry bootstrap/verification (e.g., bootstrap_and_verify_custom_sender) to
register and verify payloads; replace the inline blocks in
test_custom_sender_ed25519_transfer_e2e with calls to these helpers so the test
body stays <70 lines.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e1c14bf5-fb37-41e2-8788-b5b32bf45319

📥 Commits

Reviewing files that changed from the base of the PR and between c6d8699 and cf7bbd2.

📒 Files selected for processing (1)
  • bin/testapp/tests/mempool_e2e.rs

Comment on lines +57 to +79
impl SignatureVerifierDyn for Ed25519PayloadVerifier {
fn verify(&self, payload: &TxPayload) -> SdkResult<()> {
let TxPayload::Custom(bytes) = payload else {
return Err(ErrorCode::new(0x75));
};
let intent: EthIntentPayload =
borsh::from_slice(bytes).map_err(|_| ErrorCode::new(0x76))?;
let decoded: Ed25519EthIntentProof =
borsh::from_slice(&intent.auth_proof).map_err(|_| ErrorCode::new(0x77))?;
let envelope = intent.decode_envelope().map_err(|_| ErrorCode::new(0x78))?;
let invoke_request = envelope
.to_invoke_requests()
.into_iter()
.next()
.ok_or_else(|| ErrorCode::new(0x79))?;
let request_digest = keccak256(&invoke_request.encode().map_err(|_| ErrorCode::new(0x7A))?);
let public_key =
VerificationKey::try_from(decoded.public_key).map_err(|_| ErrorCode::new(0x7B))?;
let signature = ed25519_consensus::Signature::from(decoded.signature);
public_key
.verify(&signature, &request_digest)
.map_err(|_| ErrorCode::new(0x7C))
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bind account auth to the same intent digest the verifier checks.

Ed25519PayloadVerifier proves intent.auth_proof against the invoke-request digest, but Ed25519AuthAccount::authenticate() separately trusts payload.message_digest from authentication_payload. Nothing here enforces that those digests/signatures/public keys describe the same intent, so a crafted context can mix a valid intent proof for tx B with an unrelated account-auth signature and still pass both stages. Make the signed preimage canonical and shared by both checks (at least nonce + intent digest + sender binding), or recompute the digest inside authenticate() instead of accepting it from the caller.

Also applies to: 142-160, 582-590

🤖 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 57 - 79, The verifier
currently checks intent.auth_proof against the invoke-request digest in
Ed25519PayloadVerifier::verify but Ed25519AuthAccount::authenticate trusts
payload.message_digest from the caller, allowing mismatched signatures; fix by
making both sides use the same canonical digest: compute the canonical preimage
(e.g., nonce + request_digest + sender binding) when verifying the intent in
Ed25519PayloadVerifier::verify and either (A) pass that computed digest into
Ed25519AuthAccount::authenticate (do not accept payload.message_digest from
caller), or (B) have Ed25519AuthAccount::authenticate recompute the digest
internally from the intent/envelope (not from authentication_payload) and verify
the account signature against that recomputed digest; ensure the unique symbols
involved are Ed25519PayloadVerifier::verify, intent.auth_proof, request_digest,
Ed25519AuthAccount::authenticate, payload.message_digest and
authentication_payload so both checks validate the same preimage.

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