@@ -4,7 +4,7 @@ use crate::{
4
4
utils:: extract_signature_components,
5
5
} ;
6
6
use alloy:: {
7
- consensus:: { SimpleCoder , constants:: GWEI_TO_WEI } ,
7
+ consensus:: { SimpleCoder , Transaction , constants:: GWEI_TO_WEI } ,
8
8
eips:: BlockNumberOrTag ,
9
9
network:: { TransactionBuilder , TransactionBuilder4844 } ,
10
10
primitives:: { FixedBytes , TxHash , U256 } ,
@@ -22,9 +22,9 @@ use signet_sim::BuiltBlock;
22
22
use signet_types:: { SignRequest , SignResponse } ;
23
23
use signet_zenith:: {
24
24
BundleHelper :: { self , BlockHeader , FillPermit2 , submitCall} ,
25
- Zenith :: IncorrectHostBlock ,
25
+ Zenith :: { self , IncorrectHostBlock } ,
26
26
} ;
27
- use std:: time:: Instant ;
27
+ use std:: time:: { Instant , UNIX_EPOCH } ;
28
28
use tokio:: { sync:: mpsc, task:: JoinHandle } ;
29
29
30
30
macro_rules! spawn_provider_send {
@@ -53,17 +53,14 @@ pub enum ControlFlow {
53
53
}
54
54
55
55
/// Submits sidecars in ethereum txns to mainnet ethereum
56
- #[ derive( Debug ) ]
56
+ #[ derive( Debug , Clone ) ]
57
57
pub struct SubmitTask {
58
58
/// Zenith
59
59
pub zenith : ZenithInstance ,
60
-
61
60
/// Quincey
62
61
pub quincey : Quincey ,
63
-
64
62
/// Config
65
63
pub config : crate :: config:: BuilderConfig ,
66
-
67
64
/// Channel over which to send pending transactions
68
65
pub outbound_tx_channel : mpsc:: UnboundedSender < TxHash > ,
69
66
}
@@ -99,47 +96,69 @@ impl SubmitTask {
99
96
v : u8 ,
100
97
r : FixedBytes < 32 > ,
101
98
s : FixedBytes < 32 > ,
102
- in_progress : & BuiltBlock ,
99
+ block : & BuiltBlock ,
103
100
) -> eyre:: Result < TransactionRequest > {
104
101
let data = submitCall { fills, header, v, r, s } . abi_encode ( ) ;
105
102
106
- let sidecar = in_progress . encode_blob :: < SimpleCoder > ( ) . build ( ) ?;
103
+ let sidecar = block . encode_blob :: < SimpleCoder > ( ) . build ( ) ?;
107
104
Ok ( TransactionRequest :: default ( )
108
105
. with_blob_sidecar ( sidecar)
109
106
. with_input ( data)
110
107
. with_max_priority_fee_per_gas ( ( GWEI_TO_WEI * 16 ) as u128 ) )
111
108
}
112
109
113
- /// Returns the next host block height
110
+ /// Returns the next host block height.
114
111
async fn next_host_block_height ( & self ) -> eyre:: Result < u64 > {
115
112
let result = self . provider ( ) . get_block_number ( ) . await ?;
116
113
let next = result. checked_add ( 1 ) . ok_or_else ( || eyre ! ( "next host block height overflow" ) ) ?;
117
114
Ok ( next)
118
115
}
119
116
120
- /// Submits the EIP 4844 transaction to the network
117
+ /// Prepares and then sends the EIP- 4844 transaction with a sidecar encoded with a rollup block to the network.
121
118
async fn submit_transaction (
122
119
& self ,
120
+ retry_count : usize ,
123
121
resp : & SignResponse ,
124
- in_progress : & BuiltBlock ,
122
+ block : & BuiltBlock ,
125
123
) -> eyre:: Result < ControlFlow > {
124
+ let tx = self . prepare_tx ( retry_count, resp, block) . await ?;
125
+
126
+ self . send_transaction ( resp, tx) . await
127
+ }
128
+
129
+ /// Prepares the transaction by extracting the signature components, creating the transaction
130
+ /// request, and simulating the transaction with a call to the host provider.
131
+ async fn prepare_tx (
132
+ & self ,
133
+ retry_count : usize ,
134
+ resp : & SignResponse ,
135
+ block : & BuiltBlock ,
136
+ ) -> Result < TransactionRequest , eyre:: Error > {
137
+ // Extract the signature components from the response
126
138
let ( v, r, s) = extract_signature_components ( & resp. sig ) ;
127
139
128
- let header = BlockHeader {
129
- hostBlockNumber : resp. req . host_block_number ,
130
- rollupChainId : U256 :: from ( self . config . ru_chain_id ) ,
131
- gasLimit : resp. req . gas_limit ,
132
- rewardAddress : resp. req . ru_reward_address ,
133
- blockDataHash : * in_progress. contents_hash ( ) ,
134
- } ;
140
+ // Create the transaction request with the signature values
141
+ let tx: TransactionRequest = self . tx_request ( retry_count, resp, block, v, r, s) . await ?;
135
142
136
- let fills = vec ! [ ] ; // NB: ignored until fills are implemented
137
- let tx = self
138
- . build_blob_tx ( fills, header, v, r, s, in_progress) ?
139
- . with_from ( self . provider ( ) . default_signer_address ( ) )
140
- . with_to ( self . config . builder_helper_address )
141
- . with_gas_limit ( 1_000_000 ) ;
143
+ // Simulate the transaction with a call to the host provider
144
+ if let Some ( maybe_error) = self . sim_with_call ( & tx) . await {
145
+ warn ! (
146
+ error = ?maybe_error,
147
+ "error in transaction simulation"
148
+ ) ;
149
+ if let Err ( e) = maybe_error {
150
+ return Err ( e) ;
151
+ }
152
+ }
153
+
154
+ Ok ( tx)
155
+ }
142
156
157
+ /// Simulates the transaction with a call to the host provider to check for reverts.
158
+ async fn sim_with_call (
159
+ & self ,
160
+ tx : & TransactionRequest ,
161
+ ) -> Option < Result < ControlFlow , eyre:: Error > > {
143
162
if let Err ( TransportError :: ErrorResp ( e) ) =
144
163
self . provider ( ) . call ( tx. clone ( ) ) . block ( BlockNumberOrTag :: Pending . into ( ) ) . await
145
164
{
@@ -154,16 +173,79 @@ impl SubmitTask {
154
173
. map ( |data| data. starts_with ( & IncorrectHostBlock :: SELECTOR ) )
155
174
. unwrap_or_default ( )
156
175
{
157
- return Ok ( ControlFlow :: Retry ) ;
176
+ debug ! ( %e, "incorrect host block" ) ;
177
+ return Some ( Ok ( ControlFlow :: Retry ) ) ;
158
178
}
159
179
160
- return Ok ( ControlFlow :: Skip ) ;
180
+ if e. as_revert_data ( )
181
+ . map ( |data| data. starts_with ( & Zenith :: BadSignature :: SELECTOR ) )
182
+ . unwrap_or_default ( )
183
+ {
184
+ debug ! ( %e, "bad signature" ) ;
185
+ return Some ( Ok ( ControlFlow :: Skip ) ) ;
186
+ }
187
+
188
+ if e. as_revert_data ( )
189
+ . map ( |data| data. starts_with ( & Zenith :: OneRollupBlockPerHostBlock :: SELECTOR ) )
190
+ . unwrap_or_default ( )
191
+ {
192
+ debug ! ( %e, "one rollup block per host block" ) ;
193
+ return Some ( Ok ( ControlFlow :: Skip ) ) ;
194
+ }
195
+
196
+ return Some ( Ok ( ControlFlow :: Skip ) ) ;
161
197
}
162
198
163
- // All validation checks have passed, send the transaction
164
- self . send_transaction ( resp , tx ) . await
199
+ debug ! ( ?tx , "successfully simulated transaction request" ) ;
200
+ None
165
201
}
166
202
203
+ /// Creates a transaction request for the blob with the given header and signature values.
204
+ async fn tx_request (
205
+ & self ,
206
+ retry_count : usize ,
207
+ resp : & SignResponse ,
208
+ block : & BuiltBlock ,
209
+ v : u8 ,
210
+ r : FixedBytes < 32 > ,
211
+ s : FixedBytes < 32 > ,
212
+ ) -> Result < TransactionRequest , eyre:: Error > {
213
+ // TODO: ENG-1082 Implement fills
214
+ let fills = vec ! [ ] ;
215
+
216
+ // Bump gas with each retry to replace the previous transaction while maintaining the same nonce
217
+ let gas_coefficient = 10 * ( retry_count + 1 ) as u64 ;
218
+ let gas_limit = 1_000_000 + ( gas_coefficient * 1_000_000 ) ;
219
+ debug ! ( retry_count, gas_coefficient, gas_limit, "calculated gas limit" ) ;
220
+
221
+ // Build the block header
222
+ let header: BlockHeader = BlockHeader {
223
+ hostBlockNumber : resp. req . host_block_number ,
224
+ rollupChainId : U256 :: from ( self . config . ru_chain_id ) ,
225
+ gasLimit : resp. req . gas_limit ,
226
+ rewardAddress : resp. req . ru_reward_address ,
227
+ blockDataHash : * block. contents_hash ( ) ,
228
+ } ;
229
+ debug ! ( ?header, "built block header" ) ;
230
+
231
+ // manually retrieve nonce
232
+ let nonce =
233
+ self . provider ( ) . get_transaction_count ( self . provider ( ) . default_signer_address ( ) ) . await ?;
234
+ debug ! ( nonce, "manually setting transaction nonce" ) ;
235
+
236
+ // Create a blob transaction with the blob header and signature values and return it
237
+ let tx = self
238
+ . build_blob_tx ( fills, header, v, r, s, block) ?
239
+ . with_from ( self . provider ( ) . default_signer_address ( ) )
240
+ . with_to ( self . config . builder_helper_address )
241
+ . with_gas_limit ( gas_limit) ;
242
+
243
+ debug ! ( ?tx, "prepared transaction request" ) ;
244
+ Ok ( tx)
245
+ }
246
+
247
+ /// Fills the transaction request with the provider and sends it to the network
248
+ /// and any additionally configured broadcast providers.
167
249
async fn send_transaction (
168
250
& self ,
169
251
resp : & SignResponse ,
@@ -172,17 +254,21 @@ impl SubmitTask {
172
254
debug ! (
173
255
host_block_number = %resp. req. host_block_number,
174
256
gas_limit = %resp. req. gas_limit,
257
+ nonce = ?tx. nonce,
175
258
"sending transaction to network"
176
259
) ;
260
+
177
261
262
+ // assign the nonce and fill the rest of the values
178
263
let SendableTx :: Envelope ( tx) = self . provider ( ) . fill ( tx) . await ? else {
179
264
bail ! ( "failed to fill transaction" )
180
265
} ;
266
+ debug ! ( tx_hash = %tx. tx_hash( ) , nonce = ?tx. nonce( ) , gas_limit = ?tx. gas_limit( ) , blob_gas_used = ?tx. blob_gas_used( ) , "filled blob transaction" ) ;
181
267
182
- // Send the tx via the primary host_provider
268
+ // send the tx via the primary host_provider
183
269
let fut = spawn_provider_send ! ( self . provider( ) , & tx) ;
184
270
185
- // Spawn send_tx futures for all additional broadcast host_providers
271
+ // spawn send_tx futures for all additional broadcast host_providers
186
272
for host_provider in self . config . connect_additional_broadcast ( ) {
187
273
spawn_provider_send ! ( & host_provider, & tx) ;
188
274
}
@@ -209,9 +295,14 @@ impl SubmitTask {
209
295
Ok ( ControlFlow :: Done )
210
296
}
211
297
298
+ /// Handles the inbound block by constructing a signature request and submitting the transaction.
212
299
#[ instrument( skip_all) ]
213
- async fn handle_inbound ( & self , block : & BuiltBlock ) -> eyre:: Result < ControlFlow > {
214
- info ! ( txns = block. tx_count( ) , "handling inbound block" ) ;
300
+ async fn handle_inbound (
301
+ & self ,
302
+ retry_count : usize ,
303
+ block : & BuiltBlock ,
304
+ ) -> eyre:: Result < ControlFlow > {
305
+ info ! ( retry_count, txns = block. tx_count( ) , "handling inbound block" ) ;
215
306
let Ok ( sig_request) = self . construct_sig_request ( block) . await . inspect_err ( |e| {
216
307
error ! ( error = %e, "error constructing signature request" ) ;
217
308
} ) else {
@@ -226,47 +317,76 @@ impl SubmitTask {
226
317
227
318
let signed = self . quincey . get_signature ( & sig_request) . await ?;
228
319
229
- self . submit_transaction ( & signed, block) . await
320
+ self . submit_transaction ( retry_count , & signed, block) . await
230
321
}
231
322
323
+ /// Handles the retry logic for the inbound block.
232
324
async fn retrying_handle_inbound (
233
325
& self ,
234
326
block : & BuiltBlock ,
235
327
retry_limit : usize ,
236
328
) -> eyre:: Result < ControlFlow > {
237
329
let mut retries = 0 ;
238
330
let building_start_time = Instant :: now ( ) ;
331
+ let ( current_slot, start, end) = self . calculate_slot_window ( ) ?;
332
+ debug ! ( current_slot, start, end, "calculating target slot window" ) ;
239
333
334
+ // Retry loop
240
335
let result = loop {
241
336
let span = debug_span ! ( "SubmitTask::retrying_handle_inbound" , retries) ;
337
+ debug ! ( retries, "number of retries" ) ;
242
338
243
- let result =
244
- self . handle_inbound ( block) . instrument ( span. clone ( ) ) . await . inspect_err ( |e| {
245
- error ! ( error = %e, "error handling inbound block" ) ;
246
- } ) ?;
339
+ let inbound_result = match self . handle_inbound ( retries, block) . instrument ( span. clone ( ) ) . await {
340
+ Ok ( control_flow) => {
341
+ debug ! ( ?control_flow, retries, "successfully handled inbound block" ) ;
342
+ control_flow
343
+ }
344
+ Err ( err) => {
345
+ retries += 1 ;
346
+ error ! ( error = %err, "error handling inbound block" ) ;
347
+
348
+ if err. to_string ( ) . contains ( "403" ) {
349
+ debug ! ( "403 error - skipping block" ) ;
350
+ let ( slot_number, start, end) = self . calculate_slot_window ( ) ?;
351
+ debug ! ( slot_number, start, end, "403 sleep until skipping block" ) ;
352
+ // TODO: Sleep until the end of the next slot and return retry
353
+ return Ok ( ControlFlow :: Done ) ;
354
+ }
355
+ ControlFlow :: Retry
356
+ }
357
+ } ;
247
358
248
359
let guard = span. entered ( ) ;
249
360
250
- match result {
361
+ match inbound_result {
251
362
ControlFlow :: Retry => {
252
- retries += 1 ;
253
363
if retries > retry_limit {
254
364
counter ! ( "builder.building_too_many_retries" ) . increment ( 1 ) ;
365
+ debug ! ( "retries exceeded - skipping block" ) ;
255
366
return Ok ( ControlFlow :: Skip ) ;
256
367
}
257
- error ! ( "error handling inbound block: retrying" ) ;
258
368
drop ( guard) ;
259
- tokio:: time:: sleep ( tokio:: time:: Duration :: from_secs ( 2 ) ) . await ;
260
369
370
+ // Detect a slot change and break out of the loop in that case too
371
+ let ( this_slot, start, end) = self . calculate_slot_window ( ) ?;
372
+ if this_slot != current_slot {
373
+ debug ! ( "slot changed - skipping block" ) ;
374
+ break inbound_result;
375
+ }
376
+
377
+ // Otherwise retry the block
378
+ debug ! ( retries, this_slot, start, end, "retrying block" ) ;
261
379
continue ;
262
380
}
263
381
ControlFlow :: Skip => {
264
382
counter ! ( "builder.skipped_blocks" ) . increment ( 1 ) ;
265
- break result;
383
+ debug ! ( retries, "skipping block" ) ;
384
+ break inbound_result;
266
385
}
267
386
ControlFlow :: Done => {
268
387
counter ! ( "builder.submitted_successful_blocks" ) . increment ( 1 ) ;
269
- break result;
388
+ debug ! ( retries, "successfully submitted block" ) ;
389
+ break inbound_result;
270
390
}
271
391
}
272
392
} ;
@@ -278,16 +398,38 @@ impl SubmitTask {
278
398
Ok ( result)
279
399
}
280
400
401
+ /// Calculates and returns the slot number and its start and end timestamps for the current instant.
402
+ fn calculate_slot_window ( & self ) -> eyre:: Result < ( u64 , u64 , u64 ) > {
403
+ let now_ts = self . now ( ) ;
404
+ let current_slot = self . config . slot_calculator . calculate_slot ( now_ts) ;
405
+ let ( start, end) = self . config . slot_calculator . calculate_slot_window ( current_slot) ;
406
+ Ok ( ( current_slot, start, end) )
407
+ }
408
+
409
+ /// Returns the current timestamp in seconds since the UNIX epoch.
410
+ fn now ( & self ) -> u64 {
411
+ let now = std:: time:: SystemTime :: now ( ) ;
412
+ now. duration_since ( UNIX_EPOCH ) . unwrap ( ) . as_secs ( )
413
+ }
414
+
415
+ /// Task future for the submit task
281
416
async fn task_future ( self , mut inbound : mpsc:: UnboundedReceiver < BuiltBlock > ) {
282
417
loop {
283
418
let Some ( block) = inbound. recv ( ) . await else {
284
419
debug ! ( "upstream task gone" ) ;
285
420
break ;
286
421
} ;
422
+ debug ! ( ?block, "submit channel received block" ) ;
423
+
424
+ // TODO: Pass a BlockEnv to this function to give retrying handle inbound access to the block
425
+ // env and thus the block number so that we can be sure that we try for only our assigned slots.
287
426
427
+ // Instead this needs to fire off a task that attempts to land the block for the given slot
428
+ // Once that slot is up, it's invalid for the next anyway, so this job can be ephemeral.
288
429
if self . retrying_handle_inbound ( & block, 3 ) . await . is_err ( ) {
430
+ debug ! ( "error handling inbound block" ) ;
289
431
continue ;
290
- }
432
+ } ;
291
433
}
292
434
}
293
435
0 commit comments