|
5 | 5 | crand "crypto/rand" |
6 | 6 | "errors" |
7 | 7 | "testing" |
| 8 | + "testing/synctest" |
8 | 9 | "time" |
9 | 10 |
|
10 | 11 | "github.com/ipfs/go-datastore" |
@@ -189,101 +190,103 @@ func TestNewAggregatorComponents_Creation(t *testing.T) { |
189 | 190 | func TestExecutor_RealExecutionClientFailure_StopsNode(t *testing.T) { |
190 | 191 | // This test verifies that when the executor's execution client calls fail, |
191 | 192 | // the error is properly propagated through the error channel and stops the node |
192 | | - |
193 | | - ds := sync.MutexWrap(datastore.NewMapDatastore()) |
194 | | - memStore := store.New(ds) |
195 | | - |
196 | | - cfg := config.DefaultConfig() |
197 | | - cfg.Node.BlockTime.Duration = 50 * time.Millisecond // Fast for testing |
198 | | - |
199 | | - // Create test signer |
200 | | - priv, _, err := crypto.GenerateEd25519Key(crand.Reader) |
201 | | - require.NoError(t, err) |
202 | | - testSigner, err := noop.NewNoopSigner(priv) |
203 | | - require.NoError(t, err) |
204 | | - addr, err := testSigner.GetAddress() |
205 | | - require.NoError(t, err) |
206 | | - |
207 | | - gen := genesis.Genesis{ |
208 | | - ChainID: "test-chain", |
209 | | - InitialHeight: 1, |
210 | | - StartTime: time.Now().Add(-time.Second), // Start in past to trigger immediate execution |
211 | | - ProposerAddress: addr, |
212 | | - } |
213 | | - |
214 | | - // Create mock executor that will fail on ExecuteTxs |
215 | | - mockExec := testmocks.NewMockExecutor(t) |
216 | | - mockSeq := testmocks.NewMockSequencer(t) |
217 | | - daClient := testmocks.NewMockClient(t) |
218 | | - daClient.On("GetHeaderNamespace").Return(datypes.NamespaceFromString("ns").Bytes()).Maybe() |
219 | | - daClient.On("GetDataNamespace").Return(datypes.NamespaceFromString("data-ns").Bytes()).Maybe() |
220 | | - daClient.On("GetForcedInclusionNamespace").Return([]byte(nil)).Maybe() |
221 | | - daClient.On("HasForcedInclusionNamespace").Return(false).Maybe() |
222 | | - |
223 | | - // Mock InitChain to succeed initially |
224 | | - mockExec.On("InitChain", mock.Anything, mock.Anything, mock.Anything, mock.Anything). |
225 | | - Return([]byte("state-root"), nil).Once() |
226 | | - |
227 | | - // Mock SetDAHeight to be called during initialization |
228 | | - mockSeq.On("SetDAHeight", uint64(0)).Return().Once() |
229 | | - |
230 | | - // Mock GetNextBatch to return empty batch |
231 | | - mockSeq.On("GetNextBatch", mock.Anything, mock.Anything). |
232 | | - Return(&coresequencer.GetNextBatchResponse{ |
233 | | - Batch: &coresequencer.Batch{Transactions: nil}, |
234 | | - Timestamp: time.Now(), |
235 | | - }, nil).Maybe() |
236 | | - |
237 | | - // Mock GetTxs for reaper (return empty to avoid interfering with test) |
238 | | - mockExec.On("GetTxs", mock.Anything). |
239 | | - Return([][]byte{}, nil).Maybe() |
240 | | - |
241 | | - // Mock ExecuteTxs to fail with a critical error |
242 | | - criticalError := errors.New("execution client RPC connection failed") |
243 | | - mockExec.On("ExecuteTxs", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). |
244 | | - Return(nil, criticalError).Maybe() |
245 | | - |
246 | | - // Create aggregator node |
247 | | - components, err := NewAggregatorComponents( |
248 | | - cfg, |
249 | | - gen, |
250 | | - memStore, |
251 | | - mockExec, |
252 | | - mockSeq, |
253 | | - daClient, |
254 | | - testSigner, |
255 | | - nil, // header broadcaster |
256 | | - nil, // data broadcaster |
257 | | - zerolog.Nop(), |
258 | | - NopMetrics(), |
259 | | - DefaultBlockOptions(), |
260 | | - nil, |
261 | | - ) |
262 | | - require.NoError(t, err) |
263 | | - |
264 | | - // Start should return with error when execution client fails |
265 | | - // Timeout accounts for retry delays: 3 retries × 10s timeout = ~30s plus buffer |
266 | | - ctx, cancel := context.WithTimeout(context.Background(), 35*time.Second) |
267 | | - defer cancel() |
268 | | - |
269 | | - // Run Start in a goroutine to handle the blocking call |
270 | | - startErrCh := make(chan error, 1) |
271 | | - go func() { |
272 | | - startErrCh <- components.Start(ctx) |
273 | | - }() |
274 | | - |
275 | | - // Wait for either the error or timeout |
276 | | - select { |
277 | | - case err = <-startErrCh: |
278 | | - // We expect an error containing the critical execution client failure |
279 | | - require.Error(t, err) |
280 | | - assert.Contains(t, err.Error(), "critical execution client failure") |
281 | | - assert.Contains(t, err.Error(), "execution client RPC connection failed") |
282 | | - case <-ctx.Done(): |
283 | | - t.Fatal("timeout waiting for critical error to propagate") |
284 | | - } |
285 | | - |
286 | | - // Clean up |
287 | | - stopErr := components.Stop() |
288 | | - assert.NoError(t, stopErr) |
| 193 | + synctest.Test(t, func(t *testing.T) { |
| 194 | + ds := sync.MutexWrap(datastore.NewMapDatastore()) |
| 195 | + memStore := store.New(ds) |
| 196 | + |
| 197 | + cfg := config.DefaultConfig() |
| 198 | + cfg.Node.BlockTime.Duration = 50 * time.Millisecond // Fast for testing |
| 199 | + |
| 200 | + // Create test signer |
| 201 | + priv, _, err := crypto.GenerateEd25519Key(crand.Reader) |
| 202 | + require.NoError(t, err) |
| 203 | + testSigner, err := noop.NewNoopSigner(priv) |
| 204 | + require.NoError(t, err) |
| 205 | + addr, err := testSigner.GetAddress() |
| 206 | + require.NoError(t, err) |
| 207 | + |
| 208 | + gen := genesis.Genesis{ |
| 209 | + ChainID: "test-chain", |
| 210 | + InitialHeight: 1, |
| 211 | + StartTime: time.Now().Add(-time.Second), // Start in past to trigger immediate execution |
| 212 | + ProposerAddress: addr, |
| 213 | + } |
| 214 | + |
| 215 | + // Create mock executor that will fail on ExecuteTxs |
| 216 | + mockExec := testmocks.NewMockExecutor(t) |
| 217 | + mockSeq := testmocks.NewMockSequencer(t) |
| 218 | + daClient := testmocks.NewMockClient(t) |
| 219 | + daClient.On("GetHeaderNamespace").Return(datypes.NamespaceFromString("ns").Bytes()).Maybe() |
| 220 | + daClient.On("GetDataNamespace").Return(datypes.NamespaceFromString("data-ns").Bytes()).Maybe() |
| 221 | + daClient.On("GetForcedInclusionNamespace").Return([]byte(nil)).Maybe() |
| 222 | + daClient.On("HasForcedInclusionNamespace").Return(false).Maybe() |
| 223 | + |
| 224 | + // Mock InitChain to succeed initially |
| 225 | + mockExec.On("InitChain", mock.Anything, mock.Anything, mock.Anything, mock.Anything). |
| 226 | + Return([]byte("state-root"), nil).Once() |
| 227 | + |
| 228 | + // Mock SetDAHeight to be called during initialization |
| 229 | + mockSeq.On("SetDAHeight", uint64(0)).Return().Once() |
| 230 | + |
| 231 | + // Mock GetNextBatch to return empty batch |
| 232 | + mockSeq.On("GetNextBatch", mock.Anything, mock.Anything). |
| 233 | + Return(&coresequencer.GetNextBatchResponse{ |
| 234 | + Batch: &coresequencer.Batch{Transactions: nil}, |
| 235 | + Timestamp: time.Now(), |
| 236 | + }, nil).Maybe() |
| 237 | + |
| 238 | + // Mock GetTxs for reaper (return empty to avoid interfering with test) |
| 239 | + mockExec.On("GetTxs", mock.Anything). |
| 240 | + Return([][]byte{}, nil).Maybe() |
| 241 | + |
| 242 | + // Mock ExecuteTxs to fail with a critical error |
| 243 | + criticalError := errors.New("execution client RPC connection failed") |
| 244 | + mockExec.On("ExecuteTxs", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). |
| 245 | + Return(nil, criticalError).Maybe() |
| 246 | + |
| 247 | + // Create aggregator node |
| 248 | + components, err := NewAggregatorComponents( |
| 249 | + cfg, |
| 250 | + gen, |
| 251 | + memStore, |
| 252 | + mockExec, |
| 253 | + mockSeq, |
| 254 | + daClient, |
| 255 | + testSigner, |
| 256 | + nil, // header broadcaster |
| 257 | + nil, // data broadcaster |
| 258 | + zerolog.Nop(), |
| 259 | + NopMetrics(), |
| 260 | + DefaultBlockOptions(), |
| 261 | + nil, |
| 262 | + ) |
| 263 | + require.NoError(t, err) |
| 264 | + |
| 265 | + // Start should return with error when execution client fails. |
| 266 | + // With synctest the fake clock advances the retry delays instantly. |
| 267 | + ctx, cancel := context.WithTimeout(t.Context(), 35*time.Second) |
| 268 | + defer cancel() |
| 269 | + |
| 270 | + // Run Start in a goroutine to handle the blocking call |
| 271 | + startErrCh := make(chan error, 1) |
| 272 | + go func() { |
| 273 | + startErrCh <- components.Start(ctx) |
| 274 | + }() |
| 275 | + |
| 276 | + // Wait for either the error or timeout |
| 277 | + synctest.Wait() |
| 278 | + select { |
| 279 | + case err = <-startErrCh: |
| 280 | + // We expect an error containing the critical execution client failure |
| 281 | + require.Error(t, err) |
| 282 | + assert.Contains(t, err.Error(), "critical execution client failure") |
| 283 | + assert.Contains(t, err.Error(), "execution client RPC connection failed") |
| 284 | + case <-ctx.Done(): |
| 285 | + t.Fatal("timeout waiting for critical error to propagate") |
| 286 | + } |
| 287 | + |
| 288 | + // Clean up |
| 289 | + stopErr := components.Stop() |
| 290 | + assert.NoError(t, stopErr) |
| 291 | + }) |
289 | 292 | } |
0 commit comments