Skip to content

Comments

feat(clearnode): ERC-1271 smart contract wallet authentication#534

Open
LevanIlashvili wants to merge 2 commits intoerc7824:mainfrom
LevanIlashvili:feat/erc1271-smart-wallet-auth
Open

feat(clearnode): ERC-1271 smart contract wallet authentication#534
LevanIlashvili wants to merge 2 commits intoerc7824:mainfrom
LevanIlashvili:feat/erc1271-smart-wallet-auth

Conversation

@LevanIlashvili
Copy link

@LevanIlashvili LevanIlashvili commented Feb 8, 2026

Summary

  • Adds ERC-1271 (isValidSignature) support as a fallback when ECDSA signature recovery fails or the recovered address doesn't match the claimed wallet
  • Enables smart contract wallets (Account Kit LightAccount, Safe, Kernel, etc.) to authenticate with ClearNode via WebSocket
  • Reuses existing blockchain RPC connections from config — zero additional configuration needed

How it works

  1. handleAuthSigVerify first attempts standard ECDSA recovery
  2. If recovery fails or address doesn't match, falls back to ERC-1271:
    • Checks if the claimed address is a contract (via eth_getCode)
    • Calls isValidSignature(bytes32,bytes) on the contract
    • Iterates all configured chains to find the contract
  3. Original signature bytes are preserved (copied before ECDSA V-byte mutation)

Files changed

File Change
clearnode/erc1271.go NewIsContract() + VerifyERC1271Signature() helpers
clearnode/signer.go Extract ComputeAuthTypedDataHash() for reuse in both ECDSA and ERC-1271 paths
clearnode/rpc_router_auth.go ERC-1271 fallback in handleAuthSigVerify()
clearnode/rpc_router.go Add EthClients map[uint32]*ethclient.Client field
clearnode/main.go Initialize ethClients from configured blockchain RPCs

Test plan

  • go build ./... passes
  • Verify EOA wallet auth still works (ECDSA path unchanged)
  • Verify smart contract wallet auth via ERC-1271 fallback
  • Verify auth fails for invalid signatures on both paths

Summary by CodeRabbit

  • New Features

    • Added ERC-1271 smart-contract wallet signature verification and cross-chain contract wallet checks.
  • Improvements

    • Authentication flow now attempts standard signature recovery first, then falls back to ERC-1271 verification when needed.
    • More robust EIP-712 typed-data hashing and signature handling with clearer error reporting.

Add support for ERC-1271 (isValidSignature) as a fallback authentication
method when ECDSA signature recovery fails or the recovered address
doesn't match the claimed wallet address. This enables smart contract
wallets (e.g. Account Kit LightAccount, Safe, etc.) to authenticate
with ClearNode.

Changes:
- New erc1271.go: IsContract() and VerifyERC1271Signature() helpers
- signer.go: Extract ComputeAuthTypedDataHash() for reuse across
  both ECDSA and ERC-1271 verification paths
- rpc_router_auth.go: ERC-1271 fallback in handleAuthSigVerify() -
  copies signature before ECDSA recovery to preserve original bytes,
  iterates configured chains to find and verify the contract wallet
- rpc_router.go: Add EthClients field to RPCRouter
- main.go: Initialize ethClients from configured blockchain RPCs
@LevanIlashvili LevanIlashvili requested a review from a team as a code owner February 8, 2026 22:56
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @LevanIlashvili, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances ClearNode's authentication capabilities by introducing support for ERC-1271 smart contract wallets. It establishes a robust fallback mechanism where, if traditional ECDSA signature recovery fails, the system attempts to verify the signature using the ERC-1271 standard. This change broadens compatibility for various smart contract wallets, allowing them to authenticate seamlessly without requiring any new RPC configurations, as it intelligently reuses existing blockchain connections.

Highlights

  • ERC-1271 Smart Contract Wallet Authentication: Implemented ERC-1271 (isValidSignature) as a fallback mechanism for signature verification when standard ECDSA recovery fails or the recovered address does not match the claimed wallet.
  • Expanded Wallet Compatibility: Enables various smart contract wallets, such as Account Kit LightAccount, Safe, and Kernel, to authenticate with ClearNode via WebSocket.
  • Efficient RPC Connection Utilization: The ERC-1271 verification process reuses existing blockchain RPC connections from the application's configuration, eliminating the need for additional setup.
  • Enhanced Authentication Flow: The handleAuthSigVerify function now prioritizes standard ECDSA recovery. If this initial attempt is unsuccessful or the address mismatch, it intelligently falls back to ERC-1271 verification by checking if the claimed address is a contract and then calling isValidSignature across all configured chains.
  • Signature Integrity: The original signature bytes are now copied before ECDSA's V-byte mutation, ensuring their integrity and availability for the ERC-1271 fallback path.
  • Code Refactoring for Reusability: The EIP-712 typed data hash computation has been extracted into a new, dedicated function (ComputeAuthTypedDataHash) to facilitate its reuse by both ECDSA and ERC-1271 verification paths.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • clearnode/erc1271.go
    • Added a new file erc1271.go to centralize ERC-1271 related logic.
    • Introduced IsContract function to determine if a given address is a deployed smart contract.
    • Implemented VerifyERC1271Signature function to perform the isValidSignature call on a contract and validate its return value against the ERC-1271 magic value.
    • Defined an ERC1271Verifier interface for abstracting contract interaction methods.
  • clearnode/main.go
    • Imported the ethclient package for Ethereum client interactions.
    • Initialized a map of ethclient.Client instances (ethClients) for each configured blockchain RPC URL.
    • Assigned the newly created ethClients map to the RPCRouter instance, making it available for ERC-1271 verification.
  • clearnode/rpc_router.go
    • Imported the ethclient package.
    • Added a new field EthClients map[uint32]*ethclient.Client to the RPCRouter struct to store Ethereum client connections for different chain IDs.
  • clearnode/rpc_router_auth.go
    • Imported the strings and common packages.
    • Modified the handleAuthSigVerify function to incorporate the ERC-1271 fallback logic.
    • Ensured the original signature is copied before ECDSA recovery to prevent its mutation and allow its use in the ERC-1271 path.
    • Implemented a conditional check: if ECDSA recovery fails or the address does not match, the system now attempts ERC-1271 verification by first checking if the address is a contract and then calling VerifyERC1271Signature across all configured chains.
    • Updated comments within handleAuthSigVerify to reflect the new dual-path verification process.
  • clearnode/signer.go
    • Refactored the RecoverAddressFromEip712Signature function.
    • Extracted the EIP-712 typed data hash computation into a new, reusable function named ComputeAuthTypedDataHash.
    • The RecoverAddressFromEip712Signature now utilizes the ComputeAuthTypedDataHash function before proceeding with ECDSA public key recovery.
Activity
  • This pull request introduces a new feature for ERC-1271 smart contract wallet authentication.
  • The author has provided a comprehensive description of the changes, detailing how the new authentication flow works, and included a detailed test plan.
  • No review comments or further activity have been recorded yet, indicating it is a new submission.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 8, 2026

📝 Walkthrough

Walkthrough

Adds ERC‑1271 smart‑contract wallet signature verification and wiring. Introduces an ERC1271Verifier interface, contract detection, and signature validation; extracts EIP‑712 hash computation; updates auth flow to try ECDSA recovery first, then fall back to cross‑chain ERC‑1271 checks.

Changes

Cohort / File(s) Summary
ERC‑1271 verifier
clearnode/erc1271.go
New file: defines ERC1271Verifier interface, IsContract, VerifyERC1271Signature, ABI encoding, constants for selector/magic, and input/return validation.
Auth & signing logic
clearnode/rpc_router_auth.go, clearnode/signer.go
Auth flow now attempts ECDSA recovery first, then falls back to cross‑chain ERC‑1271 verification; extracted ComputeAuthTypedDataHash and adjusted recovery to use it.
RPC wiring
clearnode/main.go, clearnode/rpc_router.go
Creates per‑chain ethclient.Client map and exposes RPCRouter.EthClients to enable ERC‑1271 contract checks across chains; adds ethclient import and client lifecycle handling.

Sequence Diagram

sequenceDiagram
    actor Client
    participant Router as RPCRouter
    participant Signer as Signer
    participant ECDSA as ECDSA Recovery
    participant ERC1271 as ERC‑1271 Verifier
    participant Chain as Chain Contract

    Client->>Router: handleAuthSigVerify(params, signature)
    Router->>Signer: ComputeAuthTypedDataHash(...)
    Signer-->>Router: typed_data_hash
    Router->>ECDSA: RecoverAddressFromEip712Signature(hash, sig)
    alt ECDSA succeeds & address matches
        ECDSA-->>Router: recovered_address
        Router-->>Client: verification_passed
    else ECDSA fails or mismatch
        ECDSA-->>Router: error/mismatch
        Router->>ERC1271: iterate EthClients -> VerifyERC1271Signature(contractAddr, hash, sig)
        ERC1271->>Chain: Call isValidSignature(hash, signature)
        alt returns magic value
            Chain-->>ERC1271: magic_value
            ERC1271-->>Router: verification_passed
            Router-->>Client: verification_passed (contract wallet)
        else no magic value
            Chain-->>ERC1271: no_magic
            ERC1271-->>Router: continue
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • philanton

Poem

🐰 Hop, nibble, and a curious twitch,

I checked the bytes, the hash, the switch.
ECDSA first, contracts on the roam,
Cross‑chain bunnies bring signatures home. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding ERC-1271 smart contract wallet authentication support to ClearNode, which aligns with the primary objective of enabling smart contract wallets to authenticate via a fallback verification mechanism.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
clearnode/rpc_router_auth.go (2)

231-249: Consider adding a per-chain timeout to ERC-1271 RPC calls.

The IsContract and VerifyERC1271Signature calls each hit an external blockchain RPC. If any configured RPC endpoint is slow or unresponsive, it will block the authentication flow for this user — potentially for the full lifetime of ctx (which may have no deadline at all).

Consider wrapping each iteration with a per-chain timeout:

♻️ Suggested approach
 		if hashErr == nil {
 			// Try ERC-1271 verification on all configured chains
 			for chainID, client := range r.EthClients {
+				chainCtx, chainCancel := context.WithTimeout(ctx, 5*time.Second)
-				isContract, cErr := IsContract(ctx, client, expectedAddr)
+				isContract, cErr := IsContract(chainCtx, client, expectedAddr)
 				if cErr != nil || !isContract {
+					chainCancel()
 					continue
 				}
 
-				verified, vErr := VerifyERC1271Signature(ctx, client, expectedAddr, typedDataHash, sig)
+				verified, vErr := VerifyERC1271Signature(chainCtx, client, expectedAddr, typedDataHash, sig)
+				chainCancel()
 				if vErr == nil && verified {

215-263: Duplicate hash computation — minor inefficiency.

RecoverAddressFromEip712Signature (line 205) internally calls ComputeAuthTypedDataHash with the same parameters. When ECDSA fails and we enter the fallback, ComputeAuthTypedDataHash is called again at line 221 with identical inputs. You could refactor RecoverAddressFromEip712Signature to accept a pre-computed hash (or return it alongside the address) to avoid the redundant EIP-712 encoding + hashing.

clearnode/main.go (2)

65-78: Consider passing ethClients into NewRPCRouter instead of post-construction injection.

Setting router.EthClients after construction (line 78) works but is fragile — any future code added to NewRPCRouter that depends on EthClients would silently see a nil map. Passing it as a constructor parameter makes the dependency explicit and prevents this class of bug.

That said, this is low-risk today since NewRPCRouter doesn't reference EthClients during construction.


67-75: Duplicate RPC connections to the same endpoints.

Lines 67–75 dial each blockchain.BlockchainRPC for ERC-1271 verification, and then lines 90–98 create NewCustody clients that likely dial the same RPC URLs again. If the Custody client exposes an *ethclient.Client, you could share the connection.

Not a pressing concern — the number of chains is typically small — but worth noting for future cleanup.


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.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request adds support for ERC-1271 smart contract wallet authentication, which is a great feature for improving user experience with smart wallets. The implementation looks solid, with a fallback mechanism that attempts standard ECDSA recovery first and then tries ERC-1271 verification across all configured chains. The code is well-structured, especially the refactoring in signer.go to reuse the typed data hash computation.

I have a couple of suggestions for improvement, mainly focused on maintainability and debuggability. One is to use the standard go-ethereum/accounts/abi package for ABI encoding to make the code more robust. The other is to add logging for a potential error case to aid in debugging. Details are in the specific comments.

Comment on lines +37 to +66
func VerifyERC1271Signature(ctx context.Context, client ERC1271Verifier, contractAddr common.Address, hash []byte, signature []byte) (bool, error) {
// ABI-encode the call: isValidSignature(bytes32 hash, bytes signature)
// Selector (4 bytes) + hash (32 bytes padded) + offset to bytes (32 bytes) + length (32 bytes) + signature data (padded to 32)
callData := make([]byte, 0, 4+32+32+32+len(signature)+32)

// Function selector
callData = append(callData, isValidSignatureSelector...)

// bytes32 hash (already 32 bytes)
if len(hash) != 32 {
return false, fmt.Errorf("hash must be 32 bytes, got %d", len(hash))
}
callData = append(callData, hash...)

// Offset to bytes parameter (always 64 = 0x40 for two fixed params)
offset := make([]byte, 32)
offset[31] = 64
callData = append(callData, offset...)

// Length of signature bytes
sigLen := make([]byte, 32)
bigLen := big.NewInt(int64(len(signature)))
bigLen.FillBytes(sigLen)
callData = append(callData, sigLen...)

// Signature data (padded to 32-byte boundary)
callData = append(callData, signature...)
if pad := len(signature) % 32; pad != 0 {
callData = append(callData, make([]byte, 32-pad)...)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The manual ABI encoding for the isValidSignature call is complex and can be error-prone. For better robustness and maintainability, I recommend using the go-ethereum/accounts/abi package. This is the standard library for this purpose in Go and would make the code simpler and less likely to contain subtle bugs related to encoding.

Comment on lines +235 to +237
if cErr != nil || !isContract {
continue
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

When IsContract returns an error (cErr != nil), it is currently ignored and the loop continues to the next chain. This could hide persistent issues with a specific chain's RPC endpoint, making debugging difficult. I recommend adding a debug log to record the error when it occurs before continuing.

                if cErr != nil {
					logger.Debug("ERC-1271: IsContract check failed", "chainID", chainID, "address", expectedAddr, "error", cErr)
					continue
				}
				if !isContract {
					continue
				}

Copy link
Contributor

@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: 2

🤖 Fix all issues with AI agents
In `@clearnode/main.go`:
- Around line 65-75: The ethclient.Client instances created into the ethClients
map are never closed, leaking RPC connections; update the shutdown path in main
(after the RPC server shutdown block where the process handles termination) to
iterate over ethClients and call Close() on each client, logging the closure
(reference ethClients map and ethclient.Client.Close) and skipping nil entries
to ensure all dialed Ethereum RPC clients are properly closed on shutdown.

In `@clearnode/rpc_router_auth.go`:
- Around line 231-247: The code silently ignores a non-nil hashErr and skips
ERC-1271 verification; before the existing if hashErr == nil block (and before
returning "invalid signature"), log the hash error (include hashErr and any
context like challenge.Address) so operators can diagnose
ComputeAuthTypedDataHash failures — update the code that calls
ComputeAuthTypedDataHash (referenced by typedDataHash and hashErr) to emit a
logger.Warn or logger.Error when hashErr != nil, then fall through as before.
🧹 Nitpick comments (6)
clearnode/rpc_router.go (1)

29-29: Consider initializing EthClients via the constructor or a functional option.

EthClients is set externally in main.go after NewRPCRouter returns. This two-step initialization means the router is briefly in an incomplete state and the field's requirement isn't visible from the constructor signature. Passing it as a parameter (or initializing to an empty map inside NewRPCRouter) would make the dependency explicit and prevent a nil-map surprise if a future caller forgets the assignment.

Not blocking — the current code is safe because the range over a nil map is a no-op.

clearnode/main.go (1)

67-74: No dial timeout — startup can hang if an RPC endpoint is unreachable.

ethclient.Dial uses context.Background() internally, so a non-responsive endpoint will block indefinitely. Consider using ethclient.DialContext with a timeout to keep startup predictable.

🛠️ Proposed fix
 for chainID, blockchain := range config.blockchains {
-    client, err := ethclient.Dial(blockchain.BlockchainRPC)
+    dialCtx, dialCancel := context.WithTimeout(context.Background(), 10*time.Second)
+    client, err := ethclient.DialContext(dialCtx, blockchain.BlockchainRPC)
+    dialCancel()
     if err != nil {
clearnode/rpc_router_auth.go (1)

233-246: Cross-chain ERC-1271 scan: consider operational implications.

Each failed ECDSA auth attempt triggers up to 2 RPC calls per configured chain (CodeAt + CallContract). With many chains, this amplifies latency and RPC load on every invalid EOA signature. This is likely acceptable for a small number of chains, but worth keeping in mind as chains are added.

A few mitigations to consider as the chain count grows:

  • Allow the client to specify a chain_id hint in the auth request to try that chain first.
  • Add a context deadline/timeout specifically for the ERC-1271 verification loop.
clearnode/signer.go (1)

133-165: Note: RecoverAddressFromEip712Signature still mutates the input sig slice.

Line 152-153 modifies sig[64] in place. The caller in rpc_router_auth.go correctly copies the signature before calling this function, but this mutation is a latent footgun for future callers. Consider documenting this in the function's godoc, or defensively copying inside the function.

This is pre-existing behavior, not introduced by this PR — just calling it out for awareness.

clearnode/erc1271.go (2)

13-18: erc1271MagicValue and isValidSignatureSelector are identical by spec — unify them.

Per ERC-1271, the magic return value is the function selector (bytes4(keccak256("isValidSignature(bytes32,bytes)"))). Having two separate definitions — one hardcoded, one computed — creates a subtle maintenance risk if either is updated independently.

♻️ Proposed simplification
-// ERC-1271 magic value returned by isValidSignature when the signature is valid.
-// bytes4(keccak256("isValidSignature(bytes32,bytes)"))
-var erc1271MagicValue = [4]byte{0x16, 0x26, 0xba, 0x7e}
-
-// isValidSignature(bytes32,bytes) selector
-var isValidSignatureSelector = crypto.Keccak256([]byte("isValidSignature(bytes32,bytes)"))[:4]
+// isValidSignatureSelector is the 4-byte function selector for
+// isValidSignature(bytes32,bytes). Per ERC-1271, this is also the
+// magic value returned on success.
+var isValidSignatureSelector = crypto.Keccak256([]byte("isValidSignature(bytes32,bytes)"))[:4]

Then at line 81, compare against isValidSignatureSelector:

-	return result[0] == erc1271MagicValue[0] &&
-		result[1] == erc1271MagicValue[1] &&
-		result[2] == erc1271MagicValue[2] &&
-		result[3] == erc1271MagicValue[3], nil
+	return result[0] == isValidSignatureSelector[0] &&
+		result[1] == isValidSignatureSelector[1] &&
+		result[2] == isValidSignatureSelector[2] &&
+		result[3] == isValidSignatureSelector[3], nil

37-37: Consider using go-ethereum/accounts/abi for ABI encoding.

The manual encoding is correct for this specific call, but using the abi package would provide encoding validation and make it easier to adapt if the interface changes. This is optional — the manual approach is fine for a stable, simple ABI like ERC-1271.

@nksazonov
Copy link
Contributor

@LevanIlashvili , thank you for this PR!
We will try to review and test it as soon as possible. In the meantime, we are preparing a new MAJOR release, that will already include these features. Please stay tuned!

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