Skip to content

RPC: add eth_signTransaction#11517

Open
svlachakis wants to merge 22 commits intomasterfrom
eth-sign-transaction
Open

RPC: add eth_signTransaction#11517
svlachakis wants to merge 22 commits intomasterfrom
eth-sign-transaction

Conversation

@svlachakis
Copy link
Copy Markdown
Contributor

Fixes Closes Resolves #11516

Changes

  • eth_signTransaction implementation

Types of changes

What types of changes does your code introduce?

  • Bugfix (a non-breaking change that fixes an issue)
  • New feature (a non-breaking change that adds functionality)
  • Breaking change (a change that causes existing functionality not to work as expected)
  • Optimization
  • Refactoring
  • Documentation update
  • Build-related changes
  • Other: Description

Testing

Requires testing

  • Yes
  • No

If yes, did you write tests?

  • Yes
  • No

Notes on testing

Optional. Remove if not applicable.

Documentation

Requires documentation update

  • Yes
  • No

If yes, link the PR to the docs update or the issue with the details labeled docs. Remove if not applicable.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 7, 2026

Claude finished @svlachakis's task in 6m 26s —— View job


PR Review: eth_signTransaction implementation

Status: Complete

  • Read changed files and gather context
  • Load repo rules (coding-style, robustness)
  • Analyze implementation correctness and security
  • Check test coverage
  • Post findings

Summary

The implementation is well-structured and follows existing Nethermind patterns. The fee cap guard, type promotion logic, and blob sidecar wiring are all reasonable. However, one High correctness issue exists in blob transaction handling, and several Medium test coverage gaps need addressing.


Findings

High

H1 — Proofs count not validated in TryAttachBlobSidecar

src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs:478–481

if (blobTx.Commitments is null || blobTx.Commitments.Length != blobTx.Blobs.Length)
    return "commitments must be provided alongside blobs (one per blob)";
if (blobTx.Proofs is null)      // ← only null check, no length check
    return "proofs must be provided alongside blobs";

Commitments is validated for count parity with Blobs, but Proofs is only validated for null. ShardBlobNetworkWrapper (and BlobProofsManagerV0.ValidateLengths) require Proofs.Length == Blobs.Length. Calling eth_signTransaction with mismatched proof count will succeed at the signing layer and return a signed raw blob transaction whose network wrapper will be rejected by the tx pool — the API says "success" but the raw bytes are unusable.

Fix: add the length check symmetrically:

if (blobTx.Proofs is null || blobTx.Proofs.Length != blobTx.Blobs.Length)
    return "proofs must be provided alongside blobs (one per blob)";

Fix this →


Medium

M1 — Nonce validation only tested for TxType.Legacy

src/Nethermind/Nethermind.JsonRpc.Test/Modules/Eth/EthRpcModuleTests.SignTransaction.cs:25–29

The missing-field parameterized test only covers TxType.Legacy for the nonce case. Because EIP1559TransactionForRpc inherits Nonce from LegacyTransactionForRpc, the nonce check via rpcTx as LegacyTransactionForRpc works for all concrete types — but this is a subtle invariant of the inheritance chain that isn't explicitly tested. Add:

[TestCase(TxType.EIP1559, "nonce", "nonce not specified", TestName = "Eip1559NonceMissing")]

M2 — No test for AccessList (EIP-2930) signing

There is no test case covering TxType.AccessList in either the happy path or the missing-field tests. While the logic works via inheritance, it should be exercised explicitly since AccessListTransactionForRpc has distinct AccessList serialization and the fee check falls through to legacy.GasPrice (not MaxFeePerGas).

M3 — Nonce check uses indirect LegacyTransactionForRpc cast for all types

EthRpcModule.cs:367–371

LegacyTransactionForRpc? legacy = rpcTx as LegacyTransactionForRpc;
if (legacy?.Nonce is null)
    return ResultWrapper<SignTransactionResult>.Fail("nonce not specified", ErrorCodes.InvalidInput);

Address from = legacy.From ?? Address.Zero;

This silently depends on the inheritance chain (EIP1559 → AccessList → Legacy). If a future tx type does not inherit from LegacyTransactionForRpc, legacy will be null, and the error would claim "nonce not specified" instead of something meaningful. Given that the nonce and from address are accessible via LegacyTransactionForRpc properties, a comment explaining this inheritance dependency would prevent accidental breakage. Alternatively, use a shared interface or check rpcTx.Gas is null style via base properties.


Low

L1 — IsTypeDefaulted getter visibility change

src/Nethermind/Nethermind.Facade/Eth/RpcTransaction/TransactionForRpc.cs:58

Changing internal bool IsTypeDefaulted { get; set; } to public bool IsTypeDefaulted { get; internal set; } makes the getter part of the public API of TransactionForRpc. This is necessary since EthRpcModule (in Nethermind.JsonRpc) needs to read it cross-assembly. But expanding public surface should be intentional; a doc comment explaining the semantics would be appropriate given this is now publicly readable.

L2 — BlobTransactionForRpc.ToTransaction() is called before sidecar attachment, but blob count validation happens inside it

BlobTransactionForRpc.ToTransaction() validates BlobVersionedHashes.Length (≥ 1, correct format, version byte), but the Blobs/Commitments/Proofs arrays supplied in TryAttachBlobSidecar are never cross-checked against BlobVersionedHashes. A user could provide 2 blob hashes but 1 blob — this would be caught only at tx pool submission. Not a security issue, but a UX/diagnostic quality one.


Correctness cross-checks

Check Result
Raw encoding flags (SkipTypedWrapping | InMempoolForm) match eth_sendRawTransaction decode flags ✅ Consistent
Fee cap applies before signing
Sidecar attached before _wallet.Sign (signature preserved over NetworkWrapper)
SecurityException from wallet caught and surfaced as auth error
Type promotion Legacy → EIP1559 copies From correctly
tx.Hash = tx.CalculateHash() called after signing
ByteArrayConverter (global) handles Raw byte[] → hex serialization
FromTransaction(tx) echo returns runtime subtype (LegacyTransactionForRpc or EIP1559TransactionForRpc) ✅ Verified by test

@svlachakis svlachakis changed the title eth_signTransaction implementation RPC: add eth_signTransaction May 7, 2026
Comment thread src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs Outdated
Comment thread src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs
@svlachakis
Copy link
Copy Markdown
Contributor Author

@claude review again

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 7, 2026

Claude finished @svlachakis's task in 6m 7s —— View job


PR Review: eth_signTransaction (re-review)

  • Read changed files
  • Load repo rules
  • Analyze implementation correctness and security
  • Check test coverage
  • Post findings

Progress from previous review

All six prior findings have been addressed:

Finding Resolution
H1 — Proofs count not length-validated ✅ Replaced per-field length checks with manager.ValidateLengths(wrapper) which validates all three arrays in one call (BlobProofsManagerV0:38–42)
M1 — Missing EIP-1559 nonce test [TestCase(TxType.EIP1559, "nonce", ...)] added at EthRpcModuleTests.SignTransaction.cs:32
M2 — No AccessList signing test ✅ AccessList covered in both error-path and happy-path round-trip tests
M3 — Silent inheritance dependency on cast ✅ Comment added at EthRpcModule.cs:367–368
L1IsTypeDefaulted public getter without documentation ✅ Doc comment added in TransactionForRpc.cs:52–59 explaining getter/setter visibility
L2 — Blob count vs BlobVersionedHashes not cross-validated manager.ValidateHashes(wrapper, tx.BlobVersionedHashes) now cross-checks

New findings

Medium

M1 — PromoteToEip1559IfDefaultLegacy promotion branch is dead code

EthRpcModule.cs:430–448

The new EIP1559TransactionForRpc { ... } block at the end of PromoteToEip1559IfDefaultLegacy is unreachable:

  • IsTypeDefaulted = true is set only when TransactionJsonConverter.GetDefaultType() is called (no type, no gasPrice, no fee discriminators in JSON)
  • GetDefaultType() is hardcoded to return typeof(EIP1559TransactionForRpc)
  • Therefore, IsTypeDefaulted = true always means the runtime type is already EIP1559TransactionForRpc
  • The guard rpcTx is AccessListTransactionForRpc or EIP1559TransactionForRpc always fires and returns early before the promotion block

Additionally, any isDefaulted = true request would already be rejected by HasFeeFields() (no fee fields present), so the function never even runs in that case. If the intent was to auto-upgrade typeless Legacy txs to EIP-1559, the converter already handles this. The dead block should either be removed or documented with a comment explaining the triggering scenario.


Low

L1 — Blob gas excluded from fee cap

EthRpcModule.cs:455

CheckTxFeeCap evaluates maxFeePerGas * gasLimit only. For blob transactions, the user also pays maxFeePerBlobGas * blobCount * bytesPerBlobGas. A blob tx with low execution gas but very high blob gas could bypass the cap check. This matches geth's reference implementation, so it's acceptable — but worth a comment noting the intentional scope.

L2 — Combined null check gives imprecise error message

EthRpcModule.cs:478–479

"blobs, commitments and proofs must all be provided" fires when only one of commitments or proofs is null. Splitting into two separate checks ("commitments must be provided alongside blobs" / "proofs must be provided alongside blobs") would give callers better diagnostics without any logic change — the length parity is validated below by manager.ValidateLengths(wrapper).


Correctness cross-checks (re-verified)

Check Result
ValidateLengths checks blobs/commitments/proofs count parity BlobProofsManagerV0:38–42
ValidateHashes called with null guard (always non-null after ToTransaction())
Encoding flags match eth_sendRawTransaction decode flags
Fee cap applied before signing
SecurityException caught and surfaced as auth error
Type promotion guards cover Blob and SetCode via AccessListTransactionForRpc inheritance
IsTypeDefaulted doc comment correctly explains cross-assembly visibility

Comment thread src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs Outdated
Comment thread src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs Outdated
@svlachakis
Copy link
Copy Markdown
Contributor Author

@claude review again, added one more feature for geth partity

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 7, 2026

Claude finished @svlachakis's task in 3m 36s —— View job


PR Review: eth_signTransaction (re-review #3)

  • Read changed files and gather context
  • Load repo rules
  • Analyze new feature and existing findings
  • Check test coverage
  • Post findings

Progress from previous reviews

All findings from reviews #1 and #2 are resolved:

Finding Resolution
H1 (r1) — Proofs count not length-validated manager.ValidateLengths(wrapper)
M1 (r1) — Missing EIP-1559 nonce test [TestCase(TxType.EIP1559, "nonce", ...)] added
M2 (r1) — No AccessList signing test ✅ AccessList covered in error-path and round-trip tests
M3 (r1) — Silent inheritance dependency on cast ✅ Comment added
L1 (r1) — IsTypeDefaulted public getter ✅ Replaced by HasExplicitType with doc comment
L2 (r1) — Blob count vs BlobVersionedHashes not cross-validated manager.ValidateHashes
M1 (r2) — PromoteToEip1559IfDefaultLegacy dead code ✅ Method replaced by PromoteLegacyToEip1559IfTypeImplicit using HasExplicitType — the promotion path is now reachable and correct
L1 (r2) — Blob gas excluded from fee cap undocumented ✅ Explanatory comment added
L2 (r2) — Combined null check gave imprecise error message ✅ Split into separate Commitments is null / Proofs is null checks

New feature: HasExplicitType geth-parity promotion

The HasExplicitType flag is set by DeriveTxType when the JSON contains an explicit "type" field (hasExplicitType = true). PromoteLegacyToEip1559IfTypeImplicit uses it to auto-promote typeless gasPrice-only requests to EIP-1559 — matching geth's eth_signTransaction behavior. The logic is clean and the test SignTransaction_LegacyShapeJson_RespectsExplicitTypePinning exercises both paths (pinned legacy stays legacy; typeless legacy promotes to EIP-1559).


Remaining findings

Low

L1 — or EIP1559TransactionForRpc in the promotion guard is a dead branch

EthRpcModule.cs:436

if (rpcTx is AccessListTransactionForRpc or EIP1559TransactionForRpc) return rpcTx;

Since EIP1559TransactionForRpc : AccessListTransactionForRpc, the second pattern is never evaluated — every EIP1559TransactionForRpc already matches AccessListTransactionForRpc. As written, a reader unfamiliar with the hierarchy might infer these are sibling classes. Either drop the redundant arm or add a comment:

// EIP1559, Blob, and SetCode all inherit from AccessListTransactionForRpc — no promotion needed.
if (rpcTx is AccessListTransactionForRpc) return rpcTx;

L2 — HasExplicitType doc comment doesn't cover the programmatic construction case

TransactionForRpc.cs:60–68

The doc says "True when the JSON request contained an explicit type field." Objects constructed in C# code (not deserialized from JSON) will always have HasExplicitType = false, which means any LegacyTransactionForRpc built in code passed through eth_signTransaction would be promoted. The flag is only meaningful on deserialized instances. A one-line caveat in the doc comment (/// <remarks>Always <c>false</c> for programmatically constructed instances; only set during JSON deserialization.</remarks>) would prevent misuse by future consumers of this public property.


Correctness cross-checks

Check Result
HasExplicitType = true when JSON has "type": "0x0" DeriveTxType:176
HasExplicitType = false when JSON omits type but has gasPrice → promotion fires
All prior tx types (AccessList, EIP1559, Blob, SetCode) inherit AccessListTransactionForRpc — guard covers them
BlobTransactionForRpc sidecar (Commitments/Proofs) are WhenWritingNull — not echoed in tx field (correct for geth parity)
RpcTxFeeCap default 1 ETH documented with units in config XML
ulong cap type matches 1e18 default without overflow

Status: mergeable. No Critical, High, or Medium findings remain. The two Low items above are polish suggestions and don't block merge.

@svlachakis
Copy link
Copy Markdown
Contributor Author

@claude review again

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 7, 2026

Claude finished @svlachakis's task in 3m 48s —— View job


PR Review: eth_signTransaction (re-review #4)

  • Read changed files and gather context
  • Load repo rules (coding-style, robustness)
  • Analyze new/remaining findings
  • Check test coverage
  • Post findings

Progress from previous reviews

All findings from reviews #1#3 are resolved:

Finding Resolution
L1 (r3) — or EIP1559TransactionForRpc redundant arm in promotion guard ✅ Removed; guard is now just rpcTx is AccessListTransactionForRpc
L2 (r3) — HasExplicitType doc missing programmatic-construction caveat <remarks> block now covers this explicitly

New findings

Low

L1 — SetCode (EIP-7702) transaction not covered in round-trip or missing-field tests

src/Nethermind/Nethermind.JsonRpc.Test/Modules/Eth/EthRpcModuleTests.SignTransaction.cs

The happy-path round-trip (SignTransaction_WhenValid_RawRoundTripsAndTxEcho) and the missing-field test both cover Legacy, AccessList, and EIP1559. SetCodeTransactionForRpc : EIP1559TransactionForRpc is registered in TransactionJsonConverter and would reach eth_signTransaction; it requires AuthorizationList to be non-null in ToTransaction(). A minimal round-trip case or at least a missing-AuthorizationList error test would confirm the signing path works end-to-end for type 4 and guard against future regressions.


Correctness cross-checks (re-verified)

Check Result
or EIP1559TransactionForRpc dead arm removed from promotion guard
HasExplicitType doc covers programmatic construction (false by default)
SetCode is EIP1559TransactionForRpc subclass → caught by AccessListTransactionForRpc guard
tx.SenderAddress set by ToTransaction() before _wallet.Sign(tx, chainId)
_wallet.Sign is void; SecurityException is correctly caught
InternalsVisibleTo placed in source file — consistent with project pattern (e.g. LogIndexFilterVisitor.cs)
FormatWeiAsEther centi cast (ulong)centi safe since centi = x % 100 ∈ [0, 99]
HasExplicitType / IsTypeDefaulted serve distinct purposes — no redundancy

Status: mergeable. No Critical, High, or Medium findings. The one Low item above is a polish suggestion and does not block merge.

@svlachakis
Copy link
Copy Markdown
Contributor Author

@claude review my refactoring, I think it's better approach and cleaner

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 7, 2026

Claude finished @svlachakis's task in 5m 20s —— View job


PR Review: eth_signTransaction (re-review #5)

  • Read changed files and gather context
  • Load repo rules (coding-style, robustness)
  • Analyze refactoring quality and correctness
  • Check for regressions from prior findings
  • Post findings

Progress from previous reviews

All prior findings resolved, including r4 L1 (SetCode now tested in both round-trip and missing-field test cases):

Finding Resolution
r4 L1 — SetCode not covered [TestCase(TxType.SetCode, ...)] added to both round-trip and missing-field tests
All r1–r3 findings ✅ Carried forward from previous reviews

Refactoring assessment

The approach is cleaner and architecturally correct on all three axes:

1. PromoteToEip1559IfTypeDefaulted moved to domain model (TransactionForRpc)

Moving the promotion logic from EthRpcModule to TransactionForRpc is the right call — the promotion is intrinsic to how a transaction understands its own type. EthRpcModule now just calls rpcTx.PromoteToEip1559IfTypeDefaulted() at line 374 without knowing the policy details. ✅

2. IsTypeDefaulted stays internal — better than prior public getter

In r1/r2, IsTypeDefaulted had temporarily been widened to public bool ... { get; internal set; } to allow cross-assembly reads from EthRpcModule. Moving the method to the same assembly as IsTypeDefaulted (both now in Nethermind.Facade) eliminates that need entirely — the property stays internal and no public API surface was permanently expanded. ✅

3. GetDefaultType static local inlined

The GetDefaultType(out bool isDefaulted) helper existed solely to set a flag that could have been set inline. Removing it and replacing with a direct isDefaulted = true + inlined fallback is clearly simpler. The new comment "No explicit type field — every branch below is defaulted" makes the logic self-documenting. ✅

4. isDefaulted = true for gasPrice-discriminated paths (intentional behavior change)

Previously, only the "no discriminators at all" fallback set isDefaulted = true. Now, ANY path without an explicit type field sets isDefaulted = true, including the gasPrice discriminator path. This is the geth-parity change that makes typeless gasPrice-only requests auto-promote to EIP-1559 in eth_signTransaction — and it's tested explicitly in SignTransaction_LegacyShapeJson_RespectsExplicitTypePinning. ✅


New findings

Low

L1 — Third guard in PromoteToEip1559IfTypeDefaulted is dead given current hierarchy

TransactionForRpc.cs:101

if (this is AccessListTransactionForRpc) return this;   // catches AccessList + all descendants
if (this is not LegacyTransactionForRpc legacy) return this;  // ← never fires

Since all registered subclasses are either LegacyTransactionForRpc or a descendant of AccessListTransactionForRpc : LegacyTransactionForRpc, any instance that passes line 100 must already be LegacyTransactionForRpc. The return this branch on line 101 is unreachable today. It's harmless defense-in-depth, but the comment on line 98 ("this single check excludes everything that's already a typed tx") could be expanded to note line 101 is a guard against hypothetical future subtypes:

// EIP-1559, Blob, and SetCode all derive from AccessListTransactionForRpc, so this single
// check excludes everything already typed. The LegacyTransactionForRpc guard below handles
// any future direct subtype that might be added without inheriting from AccessList.

L2 — isDefaulted = true also fires when "type": null is present

TransactionForRpc.cs:204

// No explicit "type" field — every branch below is "defaulted".
isDefaulted = true;

The comment says "no explicit type field", but this path is also reached when "type": null appears in the JSON (the property exists, Deserialize<TxType?> returns null, the setType is not null guard is skipped). In the previous code isDefaulted = false was set unconditionally at the top, so "type": null would produce isDefaulted = false. Now it produces isDefaulted = true, enabling promotion.

This is a minor edge-case behaviour change for malformed input ("type": null is not a valid JSON-RPC transaction); in practice it makes the handler more permissive, which is fine. But the comment could acknowledge this: "No valid explicit type field — either absent or null".


Correctness cross-checks

Check Result
PromoteToEip1559IfTypeDefaulted called before ToTransaction — fee-field check evaluates original type HasFeeFields at line 360 runs on pre-promotion tx
gasPrice-only → EIP1559 promotion: MaxFeePerGas = MaxPriorityFeePerGas = gasPrice ✅ lines 112–113
IsTypeDefaulted = true for gasPrice path enables promotion, explicit "type":"0x0" pins Legacy ✅ tested by SignTransaction_LegacyShapeJson_RespectsExplicitTypePinning
Discriminator-typed txs (Blob, SetCode) still guarded by AccessListTransactionForRpc check — no promotion
IsTypeDefaulted stays internal — no new public API surface
ResolveType pre-Berlin downgrade: gasPrice tx now has isDefaulted = true, but Type ?? default returns TxType.Legacy anyway — same result
SetCode round-trip test added
All prior findings from r1–r4 retained

Status: mergeable. No Critical, High, or Medium findings. Both Low items are comment/documentation polish and don't affect correctness or safety.

@svlachakis
Copy link
Copy Markdown
Contributor Author

@claude review again, refactored it again it was wrong before - broke the existing use case

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 7, 2026

Claude finished @svlachakis's task in 3m 34s —— View job


PR Review: eth_signTransaction (re-review #6)

  • Read changed files and gather context
  • Load repo rules (coding-style, robustness)
  • Analyze refactoring for correctness and regressions
  • Check test coverage
  • Post findings

What broke in r5 and what's fixed now

The r5 refactoring set isDefaulted = true for all paths without an explicit type field (including discriminator-matched types like AccessList, EIP-1559, Blob). That introduced a regression in ResolveType:

return spec is not null && !spec.IsEip2930Enabled && IsTypeDefaulted ? TxType.Legacy : type;

On a pre-Berlin spec (IsEip2930Enabled = false), any discriminator-matched tx ({"accessList":[]}) would have isDefaulted = true and would be downgraded to Legacy — breaking the existing tests in SpecAwareResolutionCases that assert discriminator-matched types are preserved even on Istanbul.

The fix in this commit correctly separates the three cases with explicit isDefaulted assignment on each path:

JSON shape isDefaulted Concrete type
Explicit "type" field false type-mapped
Discriminator field (accessList, maxFeePerGas, …) false discriminator-matched
gasPrice only (no discriminators) true LegacyTransactionForRpc
Nothing at all true EIP1559TransactionForRpc

The "gasPrice → isDefaulted = true" change is a no-op for ToTransaction() (Legacy type is still Legacy regardless), and enables the geth-parity auto-promotion to EIP-1559 in eth_signTransaction. The new Istanbul test case in SpecAwareResolutionCases explicitly verifies the downgrade-is-a-no-op invariant. ✅


Correctness cross-checks

Check Result
Discriminator-matched txs (accessList, maxFeePerGas, …) are NOT promoted in eth_signTransaction isDefaulted = false for these paths
gasPrice-only tx → promoted to EIP-1559 (geth parity) isDefaulted = truePromoteToEip1559IfTypeDefaulted fires
gasPrice-only tx on pre-Berlin spec in eth_call/eth_estimateGas → still Legacy ResolveType returns TxType.Legacy regardless
{"accessList":[]} on Istanbul spec → AccessList, not downgraded to Legacy isDefaulted = false now
PromoteToEip1559IfTypeDefaulted guards cover all current subclasses via AccessListTransactionForRpc
IsTypeDefaulted stays internal (no new public API surface)
All r1–r5 findings carried forward

Remaining findings

Low

L1 — Third guard in PromoteToEip1559IfTypeDefaulted still dead (carried from r5)

TransactionForRpc.cs:101

if (this is AccessListTransactionForRpc) return this;
if (this is not LegacyTransactionForRpc legacy) return this;   // ← never fires

Every registered subclass is either LegacyTransactionForRpc or a descendant of AccessListTransactionForRpc : LegacyTransactionForRpc. Any instance that clears line 100 is a plain LegacyTransactionForRpc, so line 101's return this is unreachable today. The existing comment on lines 98–99 explains the first guard but doesn't state that line 101 is intentional defence-in-depth. A brief addition makes the intent clear to the next reader:

// The second guard below is unreachable today but guards against a future direct LegacyTransactionForRpc
// subtype that doesn't inherit from AccessListTransactionForRpc.
if (this is not LegacyTransactionForRpc legacy) return this;

Status: mergeable. The r5 regression is correctly fixed, no Critical/High/Medium findings remain. The one Low item above is comment polish only.

@svlachakis
Copy link
Copy Markdown
Contributor Author

@claude review again, made a minor fix with the cast based on your finding. is it functionally equilevant?

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 7, 2026

Claude finished @svlachakis's task in 1m 34s —— View job


PR Review: eth_signTransaction (re-review #7)

  • Read changed files and gather context
  • Load repo rules (coding-style, robustness)
  • Analyze the cast fix and verify functional equivalence
  • Check for regressions from prior findings
  • Post findings

The cast change: functionally equivalent?

Yes — completely equivalent for all current types.

The only change in commit 77aba5f is in PromoteToEip1559IfTypeDefaulted (TransactionForRpc.cs:101):

// Before (r6):
if (this is not LegacyTransactionForRpc legacy) return this;

// After (this commit):
LegacyTransactionForRpc legacy = (LegacyTransactionForRpc)this;

Why they're equivalent: At that point in the method, two invariants already hold:

  1. IsTypeDefaulted = true — established by the first guard
  2. this is not AccessListTransactionForRpc — established by the second guard, which evicts EIP1559, Blob, and SetCode (all of which inherit from AccessListTransactionForRpc)

After both guards, this can only ever be a plain LegacyTransactionForRpc instance. Looking at DeriveTxType, IsTypeDefaulted = true is set only on two paths:

Path Concrete type returned Reaches cast?
gasPrice-only, no discriminators LegacyTransactionForRpc ✅ Yes — cast succeeds trivially
Absolute default (no type, no gasPrice, no discriminators) EIP1559TransactionForRpc ❌ No — caught by AccessListTransactionForRpc guard

The is not LegacyTransactionForRpc check was dead code (flagged as r6 L1); the (LegacyTransactionForRpc) cast is the cleaner expression of the same invariant.

Behavioural difference (hypothetical only): If a future type were ever registered with IsTypeDefaulted = true and it didn't descend from either AccessListTransactionForRpc or LegacyTransactionForRpc, the old code would silently return this (no promotion), while the new code throws InvalidCastException. The hard cast is actually preferable — it fails loudly rather than silently doing the wrong thing.


Status

No Critical, High, or Medium findings remain. All prior findings from r1–r6 are resolved. The two prior Low items have also been addressed:

Finding Resolution
r6 L1 — Dead is not LegacyTransactionForRpc guard ✅ Replaced with direct cast; comment updated to explain why the cast is safe

Status: mergeable. No blocking findings.

@svlachakis
Copy link
Copy Markdown
Contributor Author

@claude review again, I reverted it back you are actually wrong on your finding. The is not LegacyTransactionForRpc legacy line earns its keep by binding legacy for use below - its no-op branch is just a free safety net, no exception ceremony.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 7, 2026

Claude finished @svlachakis's task in 42s —— View job


PR Review: eth_signTransaction (re-review #9)

  • Read changed files and gather context
  • Load repo rules
  • Analyze L2 fix (SignTransactionResultrequired properties)
  • Verify L1 acknowledgement (blob round-trip test — not doing, too complex)
  • Post findings

Progress from previous review

Both r8 findings resolved/acknowledged:

Finding Resolution
r8 L2SignTransactionResult used = null! suppressors ✅ Fixed: both properties now use required keyword — compiler enforces initialization at the single call site in eth_signTransaction
r8 L1 — Blob happy-path has no round-trip test ℹ️ Acknowledged by author as too complex; error paths (ValidateLengths, ValidateHashes, null checks) are covered; correctness not affected

Correctness cross-checks (final)

Check Result
required byte[] Raw / required TransactionForRpc Tx — enforced by compiler
SignTransactionResult always fully initialized at call site (EthRpcModule.cs:406–410)
All r1–r8 findings carried forward
No new code added in this commit

Status: mergeable. No Critical, High, or Medium findings. All Low items from previous reviews are resolved or explicitly acknowledged with rationale.

Copy link
Copy Markdown
Member

@LukaszRozmej LukaszRozmej left a comment

Choose a reason for hiding this comment

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

just FYI: we deliberately didn't implement that in the past as we wanted to avoid managing user keys and security rusks that come from it

@svlachakis
Copy link
Copy Markdown
Contributor Author

svlachakis commented May 8, 2026

@LukaszRozmej fair point - looking more carefully, the signing capability itself is already exposed via eth_sendTransaction, but I think there are three real marginal differences:

  1. signed bytes appearing in response logs
  2. lost audit trail (node never sees the broadcast)
  3. a time-shifted abuse window where an attacker can stash signed txs and replay later.

Maybe ship it as:

  1. off by default behind a config flag
  2. auto-filtered from request/response logging like the engine methods
  3. with an INFO-level audit log entry per sign (from, tx hash).

Geth ships this already without any of these guardrails, so we'd end up stricter than them. WDYT?

@svlachakis svlachakis requested a review from LukaszRozmej May 8, 2026 13:37
@alexb5dh
Copy link
Copy Markdown
Contributor

alexb5dh commented May 8, 2026

off by default behind a config flag

+1

Comment thread src/Nethermind/Nethermind.JsonRpc/JsonRpcConfig.cs Outdated
Comment thread src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs Outdated
Comment thread src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs
@LukaszRozmej
Copy link
Copy Markdown
Member

@claude re-review

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 9, 2026

Claude finished @LukaszRozmej's task in 3m 56s —— View job


PR Re-review: eth_signTransaction

  • Read changed files and gather context
  • Load repo rules (coding-style, robustness)
  • Review security concerns raised in discussion thread
  • Analyze open review suggestions from alexb5dh
  • Check current state of implementation
  • Post findings

Progress on prior findings

All technical findings from reviews #1#9 remain resolved. alexb5dh's inline suggestions have been addressed:

Suggestion Status
BlobTransactionForRpc copy constructor: populate new props Blobs/Commitments/Proofs already populated from NetworkWrapper at BlobTransactionForRpc.cs:43–48
(ulong)1.Ether for RpcTxFeeCap default ✅ Already in JsonRpcConfig.cs:71
FormatWeiAsEther alternative (ToDecimal path) ✅ Acknowledged — F2 + InvariantCulture preserves Geth wire format across locales
ErrorCodes.AccountLocked for SecurityException ✅ Acknowledged — author chose InvalidInput for Geth parity

New findings from discussion thread

The comment thread raised unresolved security concerns that were discussed and agreed upon but not yet implemented.

Medium

M1 — No opt-in config flag: eth_signTransaction is always enabled when the Eth module is active

src/Nethermind/Nethermind.JsonRpc/IJsonRpcConfig.cs

LukaszRozmej noted that signing was deliberately omitted historically due to key management security risks. svlachakis then proposed shipping it "off by default behind a config flag", and alexb5dh explicitly +1'd that specific guardrail. However, no EnableEthSignTransaction (or equivalent) config item was added. The method is unconditionally exposed to any caller who can reach the Eth module.

Concretely: a default Nethermind node with JsonRpc.Enabled = true exposes eth_signTransaction to anyone who can reach port 8545. Unlike eth_sendTransaction, the signed payload exits the node without being broadcast — it can be stored and replayed at any later time. svlachakis correctly identified this as the primary marginal risk over existing endpoints. An opt-out (or opt-in) flag was the agreed mitigation.

// IJsonRpcConfig.cs
[ConfigItem(Description = "Whether to enable eth_signTransaction. Disabled by default due to key management security risks.", DefaultValue = "false")]
bool EnableEthSignTransaction { get; set; }

Then in EthRpcModule.eth_signTransaction:

if (!_rpcConfig.EnableEthSignTransaction)
    return ResultWrapper<SignTransactionResult>.Fail("eth_signTransaction is disabled", ErrorCodes.MethodNotFound);

Fix this →


Low

L1 — eth_signTransaction not added to MethodsLoggingFiltering

src/Nethermind/Nethermind.JsonRpc/JsonRpcConfig.cs:54–62

Engine methods (engine_newPayloadV*, engine_forkchoiceUpdated*) are filtered from Trace-level request logging to prevent payload bytes appearing in log files. svlachakis explicitly proposed the same treatment for eth_signTransaction ("auto-filtered from request/response logging like the engine methods"). The signed raw bytes (and the full transaction fields) currently appear in Trace logs when Trace is enabled. While Trace is off in production by default, the filter is standard practice for sensitive signing endpoints.

// JsonRpcConfig.cs MethodsLoggingFiltering default
"engine_newPayloadV1",
"engine_newPayloadV2",
"engine_newPayloadV3",
"engine_forkchoiceUpdatedV1",
"engine_forkchoiceUpdatedV2",
"flashbots_validateBuilderSubmissionV3",
"eth_signTransaction"   // ← add

L2 — No INFO-level audit log on sign

src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs:407

eth_sign (line 325) logs at Trace. svlachakis proposed an INFO-level log entry per eth_signTransaction call (from address + tx hash) to create an auditable trail — especially meaningful since signed bytes leave the node without appearing in the mempool. A single line after tx.Hash = tx.CalculateHash() would suffice:

if (_logger.IsInfo) _logger.Info($"eth_signTransaction: signed tx {tx.Hash} for {tx.SenderAddress}");

Correctness cross-checks (re-verified against current code)

Check Result
All prior technical findings (r1–r9) ✅ Carried forward
BlobTransactionForRpc copy constructor populates Blobs/Commitments/Proofs BlobTransactionForRpc.cs:43–48
RpcTxFeeCap = (ulong)1.Ether JsonRpcConfig.cs:71
FormatWeiAsEther uses F2 + InvariantCulture ✅ Intentional Geth parity
ErrorCodes.InvalidInput on SecurityException ✅ Intentional Geth parity
eth_sign uses ErrorCodes.AccountLocked — deliberate inconsistency accepted ✅ Acknowledged

Status: not yet mergeable. M1 (opt-in config flag) reflects explicit team consensus from the discussion that was never implemented. L1 and L2 are the remaining guardrails svlachakis proposed; both are Low and don't block merge on their own.

Comment thread src/Nethermind/Nethermind.JsonRpc/IJsonRpcConfig.cs
Comment on lines +165 to +168
if (result is not null)
{
result.IsTypeDefaulted = isDefaulted;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why downgrade?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The downgrade is a pre-Berlin compatibility guard. ResolveType checks IsTypeDefaulted == true && !spec.IsEip2930Enabled and forces the type to Legacy, since pre-Berlin EVMs reject typed (≥ 0x01) txs outright. The flag is set whenever the JSON request omitted the type field - covers both the gasPrice-routing path (which is already Legacy → no-op downgrade) and the absolute fallback (EIP1559 default → actually downgraded to Legacy). Explicit type: "0x2" or discriminator-routed types (accessList/maxFeePerGas) keep IsTypeDefaulted == false and pass through unchanged. Pre-Berlin chains are mostly dead, but for eth_call/eth_estimateGas against historical heights this keeps the result spec-correct.

Comment on lines +398 to +405
try
{
_wallet.Sign(tx, chainId);
}
catch (SecurityException)
{
return ResultWrapper<SignTransactionResult>.Fail("authentication needed: password or unlock", ErrorCodes.InvalidInput);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could we have exceptionless TrySign?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I tried but it's too much for this PR.

Wallet API has Sign + TrySign duplication and the catch only handles SecurityException (Clef throws InvalidOperationException, NullWallet returns null differently). A proper fix needs an IWallet redesign.

One redesign approach is:

single Sign(Hash256, Address) returning null, TrySignTransaction virtual on the interface for Clef's account_signTransaction override, SignMessage as extension. That's a larger refactor with cascading impact across IWallet impls and other RPC handlers; better as a focused follow-up than bundled here.

Will open a separate issue/PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Comment thread src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs Outdated
Comment thread src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs Outdated
Comment thread src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs
@svlachakis
Copy link
Copy Markdown
Contributor Author

@claude review again

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 9, 2026

Claude finished @svlachakis's task in 5m 4s —— View job


PR Re-review: eth_signTransaction (re-review #10)

  • Read changed files and gather context
  • Load repo rules (coding-style, robustness)
  • Analyze LukaszRozmej's inline suggestions and current state
  • Check M1 (opt-in config flag) and exception catch expansion
  • Post findings

Progress from previous reviews

Change Status
LukaszRozmej's HasFeeFields property-pattern refactor ✅ Applied in e05bc24 — matches suggestion exactly
LukaszRozmej's MultiplyOverflow overflow guard on fee cap ✅ Applied in e05bc24
LukaszRozmej's TryAttachBlobSidecar + ValidateBlobSidecarFields split ✅ Applied in e05bc24 — matches suggestion exactly
TrySign on IWallet ↩️ Applied in 441e37a, reverted in b956806 — acknowledged as too complex for this PR; follow-up issue planned
Exception catch expanded to cover all wallet failure modes 0f37644 — now catches SecurityException, InvalidOperationException, CryptographicException
M1 (r10) — No opt-in config flag ↓ Downgraded to Low — LukaszRozmej APPROVED on 2026-05-09 after the full config-flag discussion (2026-05-08), so his approval constitutes implicit acceptance of the current state

Exception catch expansion: correct

EthRpcModule.cs:407

catch (Exception ex) when (ex is SecurityException or InvalidOperationException or CryptographicException)

The three-way catch is correct and well-documented. Each arm maps to a specific wallet implementation:

  • SecurityException — keystore wallets when account is locked
  • InvalidOperationException — ClefWallet on remote-signer rejection
  • CryptographicException — default IWallet.Sign(Transaction, ulong) when Sign(Hash256, Address) returns null (e.g. NullWallet)

The ex variable is declared but not used (only the pattern is evaluated) — this is the correct C# idiom for a filtered catch without needing the exception value. ✅


Remaining findings

Low

L1 — No opt-in EnableEthSignTransaction config flag

The guardrail proposed by svlachakis in the discussion thread (off-by-default flag, +1'd by alexb5dh) is still unimplemented. However, LukaszRozmej APPROVED the PR with awareness of this proposal, so this is confirmed as a follow-up suggestion rather than a hard prerequisite. The account-unlock check (_wallet.IsUnlocked(from)) already gates access. The config flag is defense-in-depth.

L2 — eth_signTransaction not added to MethodsLoggingFiltering

JsonRpcConfig.cs:54–62

Engine methods are filtered from Trace-level logs to prevent payload bytes appearing in log files. eth_signTransaction was proposed to receive the same treatment (signed raw bytes appear in Trace when Trace is enabled). Adding "eth_signTransaction" to the default list would prevent accidental key-material exposure in verbose logs. Low priority since Trace is off in production.

L3 — No INFO-level audit log per sign

Proposed by svlachakis: a single INFO log line after tx.Hash = tx.CalculateHash() (from address + tx hash) would create an auditable trail, especially meaningful since signed bytes leave the node without entering the mempool. eth_sign (line 325) logs at Trace; a higher-visibility level would be appropriate for signing operations.


Correctness cross-checks (full re-verification)

Check Result
Exception catch covers all three wallet failure modes EthRpcModule.cs:407
LukaszRozmej's HasFeeFields pattern (EIP1559 checks both MaxFeePerGas and MaxPriorityFeePerGas) EthRpcModule.cs:423–425
MultiplyOverflow + totalFee > capWei (overflow treated as cap-exceeded) EthRpcModule.cs:439
ValidateBlobSidecarFields extracted; null checks use switch expression EthRpcModule.cs:472–479
IsTypeDefaulted = true only for gasPrice-only and absolute default — no pre-Berlin regression TransactionForRpc.cs:189,201
PromoteToEip1559IfTypeDefaultedis not LegacyTransactionForRpc legacy binding is load-bearing for GasPrice access below TransactionForRpc.cs:84
All prior technical findings from r1–r9 ✅ Carried forward

Status: mergeable. No Critical, High, or Medium findings. The three Low items above are unimplemented guardrails proposed in the discussion thread — given LukaszRozmej's APPROVE with full awareness of the discussion, these are confirmed as post-merge follow-ups rather than blockers.

@svlachakis
Copy link
Copy Markdown
Contributor Author

@claude review again

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 9, 2026

Claude finished @svlachakis's task in 2m 59s —— View job


PR Re-review: eth_signTransaction (re-review #11)

  • Read changed files and gather context
  • Load repo rules (coding-style, robustness)
  • Analyze security fixes and new changes since r10
  • Check for remaining issues from prior reviews
  • Post findings

Progress from r10

All three Low items from r10 are now addressed:

Finding Resolution
r10 L1 — No opt-in EnableEthSignTransaction config flag bool EnableEthSignTransaction { get; set; } added to IJsonRpcConfig.cs/JsonRpcConfig.cs (default false); guard added at top of eth_signTransaction returning MethodNotFound when disabled
r10 L2eth_signTransaction not in MethodsLoggingFiltering ✅ Added to default list in both IJsonRpcConfig.cs DefaultValue string and JsonRpcConfig.cs array initializer
r10 L3 — No INFO-level audit log per sign _logger.Info($"eth_signTransaction signed tx {tx.Hash} from {tx.SenderAddress}") added at EthRpcModule.cs:417 after CalculateHash()

Tests correctly updated to set ctx.Test.RpcConfig.EnableEthSignTransaction = true in all three test helpers — the new default-off behavior is properly reflected.


New findings

Low

L1 — PromoteToEip1559IfTypeDefaulted() is public API without a doc comment

src/Nethermind/Nethermind.Facade/Eth/RpcTransaction/TransactionForRpc.cs:79

The coding-style rules require doc comments for all public APIs. PromoteToEip1559IfTypeDefaulted is now public on TransactionForRpc, but IsTypeDefaulted is internal — meaning for any externally constructed instance the method is always a no-op (since IsTypeDefaulted defaults to false). Callers outside the assembly may be surprised that calling it has no effect. A <summary> and <remarks> would close that gap:

/// <summary>
/// If this transaction's type was inferred by fallback during JSON deserialization (gasPrice-only
/// or no discriminators), re-wraps it as an <see cref="EIP1559TransactionForRpc"/>.
/// Otherwise returns <c>this</c> unchanged.
/// </summary>
/// <remarks>
/// Only effective on instances deserialized by <see cref="TransactionJsonConverter"/>.
/// Programmatically constructed instances have <see cref="IsTypeDefaulted"/> = <c>false</c>
/// and will always be returned unchanged.
/// </remarks>
public TransactionForRpc PromoteToEip1559IfTypeDefaulted()

L2 — Overflow branch in CheckTxFeeCap logs an undefined fee value

src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs:444–448

if (UInt256.MultiplyOverflow(perGas, (UInt256)tx.GasLimit, out UInt256 totalFee) || totalFee > capWei)
{
    return ResultWrapper<SignTransactionResult>.Fail(
        $"tx fee ({FormatWeiAsEther(totalFee)} ether) exceeds the configured cap ...",

When MultiplyOverflow returns true, totalFee holds the truncated (wrapped) result of the multiplication — a meaningless value. The error message will display that garbage number as if it were the actual fee, which confuses callers trying to understand how far they exceeded the cap. The other MultiplyOverflow call sites in the codebase (e.g. TransactionProcessor.cs:658, OptimismTransactionProcessor.cs:109) never use the out value in the overflow path. A minimal fix:

bool overflow = UInt256.MultiplyOverflow(perGas, (UInt256)tx.GasLimit, out UInt256 totalFee);
if (overflow || totalFee > capWei)
{
    string feeStr = overflow ? "overflow" : FormatWeiAsEther(totalFee);
    return ResultWrapper<SignTransactionResult>.Fail(
        $"tx fee ({feeStr} ether) exceeds the configured cap ({FormatWeiAsEther(capWei)} ether)",
        ErrorCodes.InvalidInput);
}

Correctness cross-checks (full re-verification)

Check Result
EnableEthSignTransaction = false by default — eth_signTransaction returns MethodNotFound when disabled EthRpcModule.cs:362–363
eth_signTransaction in MethodsLoggingFiltering default — signed raw bytes suppressed from Trace logs JsonRpcConfig.cs:62, IJsonRpcConfig.cs:143
INFO-level audit log after signing: hash + sender EthRpcModule.cs:417
Exception catch covers SecurityException, InvalidOperationException, CryptographicException EthRpcModule.cs:410
DeriveTxType: isDefaulted = true only for gasPrice-only and absolute default; discriminator-matched types keep isDefaulted = false TransactionForRpc.cs:187–202
PromoteToEip1559IfTypeDefaulted guard covers all current subclasses via AccessListTransactionForRpc TransactionForRpc.cs:83
Fee cap overflow treated as cap-exceeded EthRpcModule.cs:444
Blob sidecar: ValidateBlobSidecarFields + ValidateLengths + ValidateHashes EthRpcModule.cs:459–474
Sidecar attached before _wallet.Sign EthRpcModule.cs:395–400
tx.Hash = tx.CalculateHash() after signing EthRpcModule.cs:415
SkipTypedWrapping | InMempoolForm encode flags consistent with eth_sendRawTransaction decode path
Tests updated: all helpers set EnableEthSignTransaction = true EthRpcModuleTests.SignTransaction.cs:180,188,196
All r1–r10 findings carried forward

Status: mergeable. No Critical, High, or Medium findings. Both Low items are documentation/UX polish and don't affect correctness or safety.

@svlachakis
Copy link
Copy Markdown
Contributor Author

@claude review again, L1 is no go, its not public api as you claim

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 9, 2026

Claude finished @svlachakis's task in 1m 47s —— View job


PR Re-review: eth_signTransaction (re-review #12)

  • Read TransactionForRpc.cs to verify L1 claim about method visibility
  • Full re-review of current state
  • Post findings

On the disputed r11 L1 — you're right

TransactionForRpc.cs:79PromoteToEip1559IfTypeDefaulted() is declared public, but looking at the class as a whole, none of the other public methods have doc comments either: ToTransaction, ShouldSetBaseFee, FromTransaction, RegisterTransactionType — all public, all uncommented. The codebase convention for this class is to omit doc comments on methods. My r11 L1 incorrectly applied a blanket rule; the author is right to reject it.

On r11 L2 — fixed in a7d0f5b fix overflow

EthRpcModule.cs:444–449 now correctly guards the overflow case:

bool overflow = UInt256.MultiplyOverflow(perGas, (UInt256)tx.GasLimit, out UInt256 totalFee);
if (!overflow && totalFee <= capWei) return null;
string feeStr = overflow ? "overflow" : FormatWeiAsEther(totalFee);
return ResultWrapper<SignTransactionResult>.Fail(
    $"tx fee ({feeStr} ether) exceeds the configured cap ({FormatWeiAsEther(capWei)} ether)",
    ErrorCodes.InvalidInput);

The truncated-wraparound value is no longer used in the error message when MultiplyOverflow returns true. ✅


Full correctness re-verification

Check Result
EnableEthSignTransaction = false by default; guard returns MethodNotFound when disabled EthRpcModule.cs:362–363, IJsonRpcConfig.cs:175
eth_signTransaction in MethodsLoggingFiltering default — signed bytes suppressed from Trace logs IJsonRpcConfig.cs:143
INFO audit log after signing: hash + sender EthRpcModule.cs:417
Fee cap overflow guard — wraparound multiplication treated as cap-exceeded, error string uses "overflow" EthRpcModule.cs:444–449 (a7d0f5b)
HasFeeFields property-pattern covers EIP1559 (both fields) and Legacy (GasPrice) EthRpcModule.cs:428–430
Exception catch covers SecurityException / InvalidOperationException / CryptographicException EthRpcModule.cs:410
SignTransactionResult uses required — compiler-enforced initialization SignTransactionResult.cs:12,15
IsTypeDefaulted = true only for gasPrice-only and absolute default; discriminator-matched types keep false TransactionForRpc.cs:189,201
PromoteToEip1559IfTypeDefaultedis not LegacyTransactionForRpc legacy binding load-bearing for GasPrice TransactionForRpc.cs:84
Blob sidecar: ValidateBlobSidecarFields null checks + ValidateLengths + ValidateHashes EthRpcModule.cs:458–473
Sidecar attached before _wallet.Sign; signing only sets tx.Signature, wrapper survives EthRpcModule.cs:395–400
tx.Hash = tx.CalculateHash() after signing EthRpcModule.cs:415
SkipTypedWrapping | InMempoolForm flags consistent with eth_sendRawTransaction decode path
All findings from r1–r11 carried forward

Status: mergeable. No Critical, High, or Medium findings. Both Low items from r11 are now closed — L1 disputed correctly, L2 fixed in a7d0f5b.

@svlachakis svlachakis requested a review from LukaszRozmej May 9, 2026 18:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement eth_signTransaction

3 participants