Skip to content

feat: same signer recovery path#2520

Open
f3l1ph3s wants to merge 17 commits into
mainfrom
recovery-path
Open

feat: same signer recovery path#2520
f3l1ph3s wants to merge 17 commits into
mainfrom
recovery-path

Conversation

@f3l1ph3s

@f3l1ph3s f3l1ph3s commented May 28, 2026

Copy link
Copy Markdown
Contributor

PR Type

Enhancement


Description

  • Add recover_signer method for consistent ECDSA signer recovery

  • Update RLP decoding to invoke recover_signer on deserialization

  • Modify TryFrom conversions to call recover_signer

  • Remove direct signer extraction in try_from_alloy_transaction


Diagram Walkthrough

flowchart LR
  A["TxEnvelope"] --> B["try_from_alloy_transaction (placeholder signer)"]
  B --> C["TransactionInput"]
  C -- "recover_signer" --> D["TransactionInput with actual signer"]
Loading

File Walkthrough

Relevant files
Enhancement
transaction_input.rs
Add unified ECDSA signer recovery                                               

src/eth/primitives/transaction_input.rs

  • Added recover_signer method for ECDSA recovery
  • Updated Decodable::decode to call recover_signer
  • Modified TryFrom and TryFrom to invoke recovery
  • Removed inline signer extraction in try_from_alloy_transaction, using
    placeholder ZERO
+35/-15 

@f3l1ph3s f3l1ph3s self-assigned this May 28, 2026
@f3l1ph3s
f3l1ph3s requested a review from a team as a code owner May 28, 2026 18:10
@github-actions

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Error Context Loss

Mapping the recover_signer failure to a generic rlp::DecoderError::Custom("failed to recover signer") drops the original error details and stack context, making it harder to diagnose invalid signatures during RLP deserialization.

tx_input.recover_signer().map_err(|_| rlp::DecoderError::Custom("failed to recover signer"))?;
Ok(tx_input)
Unnecessary Clone

The implementation of recover_signer clones the entire TransactionInput to perform ECDSA recovery. This may be expensive; consider refactoring to recover from a reference or consume only the minimal needed fields to avoid a full struct clone.

let alloy_tx: AlloyTransaction = self.clone().into();
let signer: Address = match alloy_tx.inner.inner().recover_signer() {

@github-actions

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Preserve recovery error context

Use anyhow::Context to preserve the original recovery error instead of converting to
a generic bail, and simplify the match to an ? error propagation. This ensures the
root cause is retained in the error chain.

src/eth/primitives/transaction_input.rs [89-95]

-let signer: Address = match alloy_tx.inner.inner().recover_signer() {
-    Ok(signer) => Address::from(signer),
-    Err(e) => {
-        tracing::warn!(reason = ?e, "failed to recover transaction signer");
-        bail!("Transaction signer cannot be recovered. Check the transaction signature is valid.");
-    }
-};
+let signer_raw = alloy_tx.inner.inner().recover_signer()
+    .with_context(|| "Transaction signer cannot be recovered. Check the transaction signature is valid.")?;
+let signer = Address::from(signer_raw);
Suggestion importance[1-10]: 7

__

Why: Using anyhow::Context improves error reporting by retaining the original recovery error and simplifies the match with ?.

Medium

@cloudwalk-review-agent cloudwalk-review-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Summary

Good refactor direction to centralize signer recovery, but this implementation changes the signing payload before recovery for typed transactions. That can derive the wrong signer (or fail recovery) for valid transactions, so this needs to recover from the original envelope/transaction data path.

Comment thread src/eth/primitives/transaction_input.rs Outdated

@cloudwalk-review-agent cloudwalk-review-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the refactor—centralizing signer recovery is a good goal. However there is one blocking correctness issue still present:

Blocking

recover_signer() reconstructs an AlloyTransaction from TransactionInput (self.clone().into()) and then recovers from that rebuilt tx.

This path is not payload-preserving for signed fields across tx types (e.g., EIP-2930 access list defaults, EIP-1559 priority fee currently mirrored from max fee, and 4844/7702-specific fields not represented in TransactionInput). That means recovery can be executed over a different signing payload than the original envelope, yielding wrong signer or false recovery failure for valid txs.

Impact: signer identity may be incorrect or rejected depending on tx type/content, which is security-sensitive and can break authorization/accounting paths.

Suggested fix: recover signer from the original alloy_rpc_types_eth::Transaction / TxEnvelope directly (as before), or make TransactionInput fully capture every signable field before doing any round-trip-based recovery.

Test gap (non-blocking but important)

Add coverage proving signer recovery matches original for at least:

  • legacy
  • EIP-2930 with non-empty access list
  • EIP-1559 with max_priority_fee_per_gas != max_fee_per_gas
  • EIP-4844 transaction

Without this, regressions in signer derivation are easy to reintroduce.

@cloudwalk-review-agent cloudwalk-review-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice fix. This addresses the earlier blocking concern by recovering the signer from the original TxEnvelope rather than from a reconstructed transaction payload.

What improved

  • recover_signer(&TxEnvelope) now uses envelope.recover_signer() directly, avoiding lossy round-trips.
  • Decodable::decode now builds TransactionInput with placeholder signer and then recovers from the decoded envelope.
  • TryFrom<ExternalTransaction> and TryFrom<AlloyTransaction> follow the same envelope-based recovery path.
  • Removed signer extraction from try_from_alloy_transaction, so construction is now clearly separated from recovery.

This keeps signer derivation consistent across origins and preserves correctness for typed txs with fields not fully represented in TransactionInput.

Non-blocking follow-up: tests are still important here; I’d add explicit coverage for legacy, 2930 (non-empty access list), 1559 (different priority/max fee), and 4844/7702 to guard this path long-term.

@carneiro-cw carneiro-cw left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We can add tests to avoid regressions for this. In the e2e testing suite try creating each existing transaction type and using all fields that alloy can use to create a signature. Right now we're blocking certain tx types though, so maybe put the conditional that blocks the tx types in rpc_server behind a #cfg[not(dev)]. This tx would need to be sent to the leader and imported to the follower, so leader-follower tests are the right place to do this.

Comment thread src/eth/primitives/transaction_input.rs Outdated
Comment thread src/eth/primitives/transaction_input.rs Outdated

@cloudwalk-review-agent cloudwalk-review-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Summary

Looks good to approve. The core correctness risk called out in prior review (recovering signer from a reconstructed transaction payload) appears addressed: signer recovery is now done from the original TxEnvelope path, and conversion/deserialization flows consistently call recover_signer before returning usable TransactionInput values.

The new leader/follower E2E regression coverage across tx types 0/1/2/3 with non-trivial fields is meaningful and directly targets the previous class of signer-recovery regressions.

I did not find concrete blocking issues in the provided diff. Remaining risk is mostly around uninspected paths not included in the excerpt (e.g., any constructors/usages of TransactionInput outside these conversion paths that might call signer() before recovery), but nothing concrete in this PR indicates a live bug.

@f3l1ph3s
f3l1ph3s requested a review from carneiro-cw June 3, 2026 18:52
Comment thread e2e/cloudwalk-contracts/integration/test/helpers/account.ts
Comment thread e2e/cloudwalk-contracts/integration/test/leader-follower-tx-types.test.ts Outdated
Comment thread src/eth/executor/evm_input.rs Outdated
Comment thread src/eth/primitives/transaction_input.rs Outdated
Comment thread src/eth/primitives/transaction_input.rs Outdated
Comment thread src/eth/primitives/transaction_input.rs Outdated

@cloudwalk-review-agent cloudwalk-review-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocking issue

The #[cfg(feature = "dev")] gate on type-4 rejection in eth_send_raw_transaction appears to invert the safety behavior and can re-enable the known follower-crash path in non-dev builds.

In this PR, the existing guard:

if tx.transaction_info.tx_type.is_some_and(|t| t > 3) {
    tracing::warn!(%tx_hash, "rejecting unsuported transaction type");
    return Err(RpcError::ParameterInvalid.into());
}

was changed to:

#[cfg(feature = "dev")]
if tx.transaction_info.tx_type.is_some_and(|t| t > 3) { ... }

Given the in-code comment says this guard is a temporary stopgap to prevent follower crashes, compiling it only when dev is enabled means production/non-dev builds no longer reject tx type 4 and may hit the crash condition.

Please switch this to the opposite condition (#[cfg(not(feature = "dev"))]) or otherwise ensure the crash-prevention guard remains active in the environments where followers are vulnerable.

@f3l1ph3s
f3l1ph3s requested a review from carneiro-cw July 8, 2026 02:45

@cloudwalk-review-agent cloudwalk-review-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocking issue

The tx-type safety guard in eth_send_raw_transaction is now compiled only with feature = "dev", which appears to invert the intended protection.

In this diff (src/eth/rpc/rpc_server.rs), the hotfix comment says this guard prevents tx type 4 from crashing followers, but the code is now:

#[cfg(feature = "dev")]
if tx.transaction_info.tx_type.is_some_and(|t| t > 3) { ... }

That means non-dev/prod builds will skip the rejection path entirely and can accept type 4 again.

If the crash condition still exists, this is deploy-blocking for non-dev environments. Please switch the cfg condition (likely #[cfg(not(feature = "dev"))]) or otherwise ensure the guard is active where followers are vulnerable.

@cloudwalk-review-agent cloudwalk-review-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocking issue

The tx-type rejection guard in eth_send_raw_transaction is now gated behind #[cfg(feature = "dev")], which appears to invert the intended safety behavior.

In src/eth/rpc/rpc_server.rs, this block:

#[cfg(feature = "dev")]
if tx.transaction_info.tx_type.is_some_and(|t| t > 3) {
    tracing::warn!(%tx_hash, "rejecting unsuported transaction type");
    return Err(RpcError::ParameterInvalid.into());
}

means non-dev/prod builds won’t execute the stopgap rejection for tx type > 3. The nearby comment says this guard exists to prevent follower crashes for type 4 transactions, so compiling it only in dev re-opens the crash path where it matters most.

Please keep this protection active in non-dev environments (e.g. invert cfg condition or remove the dev-only gate).

@cloudwalk-review-agent cloudwalk-review-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocking issue

The tx-type safety guard in eth_send_raw_transaction appears inverted by cfg-gating and is now only compiled for dev builds:

#[cfg(feature = "dev")]
if tx.transaction_info.tx_type.is_some_and(|t| t > 3) {
    tracing::warn!(%tx_hash, "rejecting unsuported transaction type");
    return Err(RpcError::ParameterInvalid.into());
}

Given the nearby comment says this is a temporary stopgap to prevent follower crashes on type-4 txs, compiling it only in dev removes that protection from non-dev/prod builds.

Impact: non-dev environments can accept tx type > 3 again and re-open the known follower-crash path.

Please keep this guard active in the vulnerable environments (e.g. invert cfg to not(feature = "dev") or remove the dev-only gate).

@cloudwalk-review-agent cloudwalk-review-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM — I did not find a concrete blocking issue in the provided diff.

Summary

  • The signer recovery path is now centralized and consistently invoked after deserialization/conversion (Decodable, TryFrom<ExternalTransaction>, TryFrom<AlloyTransaction>), which addresses the prior correctness risk of inconsistent recovery paths.
  • ExecutionInfo now carries typed-tx fields (priority fee, blob fee/hashes, access/authorization lists), and those fields are propagated through TransactionInput <-> AlloyTransaction and persisted in RocksDB mappings.
  • TransactionExecution now preserves original execution_info instead of reconstructing it from EvmInput, reducing lossy transformations across transaction types.
  • Added meaningful E2E regression coverage across tx types 0/1/2/3 in leader/follower flows, including signer checks and receipt parity.

No VictoriaLogs production logging anti-patterns were identified in backend runtime code changes (test console.log usage is test-only and allow-listed).

@carneiro-cw carneiro-cw left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As I said before, we won't be updating the rocks types for this. Instead we'll compute the singer ignoring the fields we don't support for now.

@cloudwalk-review-agent cloudwalk-review-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM.

I reviewed the signer-recovery refactor and related persistence/execution wiring changes and did not find a concrete blocking issue in the provided diff.

What looks good

  • Signer recovery is now centralized via TransactionInput and consistently used across decode/TryFrom/EVM input paths, reducing prior divergence risk.
  • The tx type > 3 safety guard in eth_sendRawTransaction is now correctly gated with #[cfg(not(feature = "dev"))], preserving non-dev protection.
  • TransactionExecution now carries execution_info, and Rocks serialization paths use it, reducing lossy reconstruction from evm_input.
  • New E2E coverage across tx types 0/1/2/3/4 (leader/follower signer + receipt parity) is meaningful and directly targets this regression class.

No concrete VictoriaLogs production logging anti-pattern was found in backend runtime code touched by this PR (test console.log usage ignored per allow-list).

@cloudwalk-review-agent cloudwalk-review-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM — I did not find a concrete blocking issue in the provided diff.

What I checked

  • Signer recovery correctness across decode/conversion/execution/storage paths
  • RPC safety behavior for tx type gating
  • Data consistency risks introduced by Signer::{Recovered,Unrecovered} and lazy signer() access
  • Regression test coverage for multi-type tx handling in leader/follower flows
  • Logging changes for VictoriaLogs anti-patterns (none in production runtime paths; test console.log is allow-listed)

Summary

  • Centralizing signer recovery via TransactionInput and reusing to_tx_envelope() removes prior path divergence and keeps behavior consistent in Decodable, TryFrom, executor, and Rocks serialization.
  • TransactionExecution now carries execution_info explicitly, avoiding lossy reconstruction from evm_input.
  • The tx-type safety guard in eth_send_raw_transaction is now #[cfg(not(feature = "dev"))], preserving non-dev protection against unsupported type > 3.
  • Added E2E coverage for signer/receipt parity across tx types (including typed tx variants) is meaningful and directly aligned with the regression class.

No concrete security/correctness/data-loss/deploy-safety issue is evidenced in the current diff beyond previously addressed concerns.

@cloudwalk-review-agent cloudwalk-review-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM.

I reviewed the signer-recovery refactor and surrounding execution/storage/RPC wiring in the provided diff and did not find a concrete blocking issue.

What looks solid

  • Signer recovery is centralized in TransactionInput (recover_signer_address/to_tx_envelope) and consistently used across decode and conversion paths.
  • ExecutionInfo.signer now models recovered-vs-unrecovered state explicitly, reducing accidental reliance on placeholder addresses.
  • TransactionExecution now carries execution_info directly, avoiding lossy reconstruction from EvmInput during round-trips.
  • Storage mappings were updated to persist/rebuild signer with the new enum shape.
  • The non-dev tx-type safety guard for tx_type > 3 is present in eth_send_raw_transaction.
  • Added/updated integration coverage for signer consistency and leader/follower parity across tx types is meaningful and aligned with the regression class.

No concrete VictoriaLogs production logging anti-pattern was identified in backend runtime paths touched by this PR (test console.log usage is allow-listed).

@cloudwalk-review-agent cloudwalk-review-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM.

I reviewed the provided diff with focus on signer recovery correctness, tx-type safety behavior, persistence consistency, and regression coverage. I did not find a concrete blocking issue in the current head.

Summary

  • Signer recovery is now centralized in TransactionInput and reused across decode/conversion/execution paths, which reduces prior divergence risk.
  • ExecutionInfo.signer now explicitly models recovered vs unrecovered state, and call sites were updated to use tx.signer() where a concrete address is required.
  • TransactionExecution now carries execution_info directly, avoiding lossy reconstruction from EvmInput and improving storage round-trip fidelity.
  • Non-dev tx-type guard for tx_type > 3 is present in eth_send_raw_transaction via #[cfg(not(feature = "dev"))], preserving the follower crash stopgap in non-dev/prod.
  • Added E2E coverage across tx types 0/1/2/3/4 for leader/follower signer and receipt parity is meaningful for this regression class.

No concrete VictoriaLogs production logging anti-patterns were identified in backend runtime code touched by this PR (test-side console.log usage is allow-listed).

@f3l1ph3s
f3l1ph3s requested a review from carneiro-cw July 17, 2026 18:25

@cloudwalk-review-agent cloudwalk-review-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM — I did not find a concrete blocking issue in the provided diff.

What I checked

  • Signer recovery correctness across decode / TryFrom / envelope reconstruction paths
  • Safety around fallback signer access (TransactionInput::signer())
  • Execution/storage round-trip consistency after introducing execution_info into TransactionExecution
  • RPC tx-type guard behavior for follower crash stopgap
  • Regression test coverage for tx types 0/1/2/3/4 leader-follower parity
  • VictoriaLogs anti-patterns in production logging paths touched by this PR

Summary

  • Centralized signer recovery via recover_signer_address() + to_tx_envelope() is consistently applied in conversion paths and avoids prior path divergence.
  • ExecutionInfo.signer becoming Signer::{Recovered,Unrecovered} is wired through executor/storage/RPC call sites without obvious contract break in touched code.
  • TransactionExecution now carries execution_info, reducing lossy reconstruction from EvmInput and improving persistence fidelity.
  • The non-dev tx-type rejection guard (#[cfg(not(feature = "dev"))] for tx_type > 3) is present in eth_send_raw_transaction, preserving the stated stopgap.
  • New/updated E2E coverage for transaction-type signer recovery and leader/follower receipt parity is meaningful and aligned with the regression risk.
  • No concrete VictoriaLogs production logging issue found (test console.log usage is allow-listed).

Comment thread e2e/cloudwalk-contracts/integration/test/leader-follower-tx-types.test.ts Outdated
Comment thread e2e/cloudwalk-contracts/integration/test/helpers/account.ts Outdated
Comment thread e2e/cloudwalk-contracts/integration/test/helpers/account.ts Outdated
Comment thread e2e/test/helpers/rpc.ts Outdated

@cloudwalk-review-agent cloudwalk-review-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM — I did not find a concrete blocking issue in the provided diff.

What I checked

  • Signer recovery correctness across decode/conversion/executor/storage/RPC paths
  • Potential behavior regressions from introducing Signer::{Recovered, Unrecovered}
  • Temporary-storage and RocksDB round-trip consistency after adding execution_info into TransactionExecution
  • Non-dev tx-type safety guard behavior in eth_send_raw_transaction
  • New/updated E2E coverage relevance for the signer-recovery regression class
  • VictoriaLogs anti-patterns in touched backend runtime logging paths

Summary

  • Centralized signer recovery in TransactionInput is wired consistently through TryFrom, RLP decode, EVM input creation, and RPC tracing usage.
  • TransactionExecution now preserving execution_info avoids lossy reconstruction from evm_input and is reflected in storage mappings.
  • The tx-type > 3 stopgap in eth_send_raw_transaction is gated for non-dev builds (#[cfg(not(feature = "dev"))]), preserving production protection.
  • E2E additions/updates meaningfully exercise multi-type signer consistency (including 7702 path) and leader/follower parity.
  • No actionable VictoriaLogs production logging issue was found in backend runtime code touched by this PR (test/debug console.log usage is in allow-listed test paths).

@f3l1ph3s
f3l1ph3s requested a review from carneiro-cw July 23, 2026 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants