@@ -80,10 +80,121 @@ pub struct L2Call {
8080 pub signal_slot_on_l2 : FixedBytes < 32 > ,
8181}
8282
83+ /// Result of routing a UserOp: either it targets L1 (and triggers an L2 bridge call)
84+ /// or it targets L2 (for direct execution on L2, e.g. bridge-out).
85+ pub enum UserOpRouting {
86+ /// L1 UserOp that triggers a bridge deposit (L1→L2).
87+ L1ToL2 { user_op : UserOp , l2_call : L2Call } ,
88+ /// L2 UserOp for direct execution on L2 (e.g. bridge-out L2→L1).
89+ L2Direct { user_op : UserOp } ,
90+ }
91+
92+ /// Determine the target chain of a UserOp by checking the EIP-712 signature.
93+ ///
94+ /// The UserOpsSubmitter uses EIP-712 with domain `(name="UserOpsSubmitter", version="1",
95+ /// chainId, verifyingContract=submitter)`. We decode the `executeBatch(ops[], signature)`
96+ /// calldata, compute the EIP-712 digest for both L1 and L2 chain IDs, and ecrecover.
97+ /// The chain ID that produces a valid recovery (non-zero address) is the target chain.
98+ fn detect_target_chain ( user_op : & UserOp , l1_chain_id : u64 , l2_chain_id : u64 ) -> Option < u64 > {
99+ use alloy:: sol;
100+ use alloy:: sol_types:: { SolCall , SolValue } ;
101+
102+ // ABI definition matching UserOpsSubmitter.executeBatch
103+ sol ! {
104+ struct UserOpSol {
105+ address target;
106+ uint256 value;
107+ bytes data;
108+ }
109+
110+ function executeBatch( UserOpSol [ ] calldata _ops, bytes calldata _signature) external;
111+ }
112+
113+ // Decode the calldata
114+ let decoded = executeBatchCall:: abi_decode ( & user_op. calldata ) . ok ( ) ?;
115+ let ops = & decoded. _ops ;
116+ let signature = & decoded. _signature ;
117+
118+ if signature. len ( ) != 65 {
119+ warn ! ( "UserOp id={}: signature length {} != 65" , user_op. id, signature. len( ) ) ;
120+ return None ;
121+ }
122+
123+ // EIP-712 type hashes
124+ let userop_typehash = alloy:: primitives:: keccak256 (
125+ b"UserOp(address target,uint256 value,bytes data)" ,
126+ ) ;
127+ let executebatch_typehash = alloy:: primitives:: keccak256 (
128+ b"ExecuteBatch(UserOp[] ops)UserOp(address target,uint256 value,bytes data)" ,
129+ ) ;
130+
131+ // Hash each op: keccak256(abi.encode(typehash, target, value, keccak256(data)))
132+ let mut op_hashes = Vec :: with_capacity ( ops. len ( ) ) ;
133+ for op in ops {
134+ let data_hash = alloy:: primitives:: keccak256 ( & op. data ) ;
135+ let encoded = ( userop_typehash, op. target , op. value , data_hash) . abi_encode ( ) ;
136+ op_hashes. push ( alloy:: primitives:: keccak256 ( & encoded) ) ;
137+ }
138+
139+ // keccak256(abi.encodePacked(opHashes))
140+ let mut packed = Vec :: with_capacity ( op_hashes. len ( ) * 32 ) ;
141+ for h in & op_hashes {
142+ packed. extend_from_slice ( h. as_slice ( ) ) ;
143+ }
144+ let ops_array_hash = alloy:: primitives:: keccak256 ( & packed) ;
145+
146+ // struct hash = keccak256(abi.encode(EXECUTEBATCH_TYPEHASH, ops_array_hash))
147+ let struct_hash = alloy:: primitives:: keccak256 (
148+ & ( executebatch_typehash, ops_array_hash) . abi_encode ( ) ,
149+ ) ;
150+
151+ // Parse the 65-byte signature
152+ let sig = alloy:: signers:: Signature :: try_from ( signature. as_ref ( ) ) . ok ( ) ?;
153+
154+ // Try both chain IDs
155+ for chain_id in [ l1_chain_id, l2_chain_id] {
156+ let domain_separator = compute_domain_separator ( chain_id, user_op. submitter ) ;
157+
158+ // EIP-712 digest: keccak256("\x19\x01" || domainSeparator || structHash)
159+ let mut digest_input = Vec :: with_capacity ( 2 + 32 + 32 ) ;
160+ digest_input. extend_from_slice ( & [ 0x19 , 0x01 ] ) ;
161+ digest_input. extend_from_slice ( domain_separator. as_slice ( ) ) ;
162+ digest_input. extend_from_slice ( struct_hash. as_slice ( ) ) ;
163+ let digest = alloy:: primitives:: keccak256 ( & digest_input) ;
164+
165+ if let Ok ( recovered) = sig. recover_address_from_prehash ( & digest) {
166+ if recovered != Address :: ZERO {
167+ info ! (
168+ "UserOp id={}: signature valid for chain_id={} (recovered={})" ,
169+ user_op. id, chain_id, recovered
170+ ) ;
171+ return Some ( chain_id) ;
172+ }
173+ }
174+ }
175+
176+ warn ! ( "UserOp id={}: could not determine target chain" , user_op. id) ;
177+ None
178+ }
179+
180+ /// Compute EIP-712 domain separator for UserOpsSubmitter(name="UserOpsSubmitter", version="1")
181+ fn compute_domain_separator ( chain_id : u64 , verifying_contract : Address ) -> B256 {
182+ use alloy:: sol_types:: SolValue ;
183+
184+ let type_hash = alloy:: primitives:: keccak256 (
185+ b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" ,
186+ ) ;
187+ let name_hash = alloy:: primitives:: keccak256 ( b"UserOpsSubmitter" ) ;
188+ let version_hash = alloy:: primitives:: keccak256 ( b"1" ) ;
189+
190+ alloy:: primitives:: keccak256 (
191+ & ( type_hash, name_hash, version_hash, alloy:: primitives:: U256 :: from ( chain_id) , verifying_contract) . abi_encode ( ) ,
192+ )
193+ }
194+
83195#[ derive( Clone ) ]
84196struct BridgeRpcContext {
85197 tx : mpsc:: Sender < UserOp > ,
86- l2_tx : mpsc:: Sender < UserOp > ,
87198 status_store : UserOpStatusStore ,
88199 next_id : Arc < AtomicU64 > ,
89200}
@@ -92,8 +203,9 @@ pub struct BridgeHandler {
92203 ethereum_l1 : Arc < EthereumL1 < ExecutionLayer > > ,
93204 taiko : Arc < Taiko > ,
94205 rx : Receiver < UserOp > ,
95- l2_rx : Receiver < UserOp > ,
96206 status_store : UserOpStatusStore ,
207+ l1_chain_id : u64 ,
208+ l2_chain_id : u64 ,
97209}
98210
99211impl BridgeHandler {
@@ -102,14 +214,14 @@ impl BridgeHandler {
102214 ethereum_l1 : Arc < EthereumL1 < ExecutionLayer > > ,
103215 taiko : Arc < Taiko > ,
104216 cancellation_token : CancellationToken ,
217+ l1_chain_id : u64 ,
218+ l2_chain_id : u64 ,
105219 ) -> Result < Self , anyhow:: Error > {
106220 let ( tx, rx) = mpsc:: channel :: < UserOp > ( 1024 ) ;
107- let ( l2_tx, l2_rx) = mpsc:: channel :: < UserOp > ( 1024 ) ;
108221 let status_store = UserOpStatusStore :: open ( "data/user_op_status" ) ?;
109222
110223 let rpc_context = BridgeRpcContext {
111224 tx,
112- l2_tx,
113225 status_store : status_store. clone ( ) ,
114226 next_id : Arc :: new ( AtomicU64 :: new ( 1 ) ) ,
115227 } ;
@@ -169,33 +281,6 @@ impl BridgeHandler {
169281 }
170282 } ) ?;
171283
172- module. register_async_method ( "surge_sendL2UserOp" , |params, ctx, _| async move {
173- let mut user_op: UserOp = params. parse ( ) ?;
174- let id = ctx. next_id . fetch_add ( 1 , Ordering :: Relaxed ) ;
175- user_op. id = id;
176-
177- info ! (
178- "Received L2 UserOp: id={}, submitter={:?}, calldata_len={}" ,
179- id,
180- user_op. submitter,
181- user_op. calldata. len( )
182- ) ;
183-
184- ctx. status_store . set ( id, & UserOpStatus :: Pending ) ;
185-
186- ctx. l2_tx . send ( user_op) . await . map_err ( |e| {
187- error ! ( "Failed to send L2 UserOp to queue: {}" , e) ;
188- ctx. status_store . remove ( id) ;
189- jsonrpsee:: types:: ErrorObjectOwned :: owned (
190- -32000 ,
191- "Failed to queue L2 user operation" ,
192- Some ( format ! ( "{}" , e) ) ,
193- )
194- } ) ?;
195-
196- Ok :: < u64 , jsonrpsee:: types:: ErrorObjectOwned > ( id)
197- } ) ?;
198-
199284 info ! ( "Bridge handler RPC server starting on {}" , addr) ;
200285 let handle = server. start ( module) ;
201286
@@ -209,47 +294,84 @@ impl BridgeHandler {
209294 ethereum_l1,
210295 taiko,
211296 rx,
212- l2_rx,
213297 status_store,
298+ l1_chain_id,
299+ l2_chain_id,
214300 } )
215301 }
216302
217303 pub fn status_store ( & self ) -> UserOpStatusStore {
218304 self . status_store . clone ( )
219305 }
220306
221- pub async fn next_user_op_and_l2_call (
307+ /// Dequeue the next UserOp and route it to the correct chain.
308+ ///
309+ /// Parses the EIP-712 signature in the `executeBatch` calldata to determine
310+ /// which chain the UserOp targets. If signed for L1, simulates on L1 to
311+ /// extract the bridge message. If signed for L2, returns it for direct
312+ /// L2 block inclusion.
313+ pub async fn next_user_op_routed (
222314 & mut self ,
223- ) -> Result < Option < ( UserOp , L2Call ) > , anyhow:: Error > {
224- if let Ok ( user_op) = self . rx . try_recv ( ) {
225- if let Some ( ( message_from_l1, signal_slot_on_l2) ) = self
226- . ethereum_l1
227- . execution_layer
228- . find_message_and_signal_slot ( user_op. clone ( ) )
229- . await ?
230- {
231- return Ok ( Some ( (
232- user_op,
233- L2Call {
234- message_from_l1,
235- signal_slot_on_l2,
315+ ) -> Result < Option < UserOpRouting > , anyhow:: Error > {
316+ let Ok ( user_op) = self . rx . try_recv ( ) else {
317+ return Ok ( None ) ;
318+ } ;
319+
320+ let target_chain = detect_target_chain ( & user_op, self . l1_chain_id , self . l2_chain_id ) ;
321+
322+ match target_chain {
323+ Some ( chain_id) if chain_id == self . l1_chain_id => {
324+ // L1 UserOp — simulate on L1 to extract bridge message
325+ if let Some ( ( message_from_l1, signal_slot_on_l2) ) = self
326+ . ethereum_l1
327+ . execution_layer
328+ . find_message_and_signal_slot ( user_op. clone ( ) )
329+ . await ?
330+ {
331+ return Ok ( Some ( UserOpRouting :: L1ToL2 {
332+ user_op,
333+ l2_call : L2Call {
334+ message_from_l1,
335+ signal_slot_on_l2,
336+ } ,
337+ } ) ) ;
338+ }
339+
340+ // L1 simulation found no bridge message — still an L1 UserOp (non-bridge)
341+ warn ! (
342+ "UserOp id={} targets L1 but no bridge message found, treating as L1 UserOp without bridge" ,
343+ user_op. id
344+ ) ;
345+ self . status_store . set (
346+ user_op. id ,
347+ & UserOpStatus :: Rejected {
348+ reason : "L1 UserOp with no bridge message" . to_string ( ) ,
236349 } ,
237- ) ) ) ;
350+ ) ;
351+ Ok ( None )
352+ }
353+ Some ( chain_id) if chain_id == self . l2_chain_id => {
354+ // L2 UserOp — execute directly on L2
355+ info ! (
356+ "UserOp id={} targets L2 (chain_id={}), queueing for L2 execution" ,
357+ user_op. id, chain_id
358+ ) ;
359+ Ok ( Some ( UserOpRouting :: L2Direct { user_op } ) )
360+ }
361+ _ => {
362+ warn ! (
363+ "UserOp id={} rejected: could not determine target chain from signature" ,
364+ user_op. id
365+ ) ;
366+ self . status_store . set (
367+ user_op. id ,
368+ & UserOpStatus :: Rejected {
369+ reason : "Could not determine target chain from signature" . to_string ( ) ,
370+ } ,
371+ ) ;
372+ Ok ( None )
238373 }
239-
240- warn ! (
241- "UserOp id={} rejected: no L2 call found in user op" ,
242- user_op. id
243- ) ;
244- self . status_store . set (
245- user_op. id ,
246- & UserOpStatus :: Rejected {
247- reason : "No L2 call found in user op" . to_string ( ) ,
248- } ,
249- ) ;
250374 }
251-
252- Ok ( None )
253375 }
254376
255377 pub async fn find_l1_call (
@@ -278,13 +400,4 @@ impl BridgeHandler {
278400 pub fn has_pending_user_ops ( & self ) -> bool {
279401 !self . rx . is_empty ( )
280402 }
281-
282- /// Dequeue the next L2 UserOp (for bridge-out / L2 execution).
283- pub fn next_l2_user_op ( & mut self ) -> Option < UserOp > {
284- self . l2_rx . try_recv ( ) . ok ( )
285- }
286-
287- pub fn has_pending_l2_user_ops ( & self ) -> bool {
288- !self . l2_rx . is_empty ( )
289- }
290403}
0 commit comments