Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/constants/stellar_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,16 @@ pub const STELLAR_MAX_STUCK_TRANSACTION_LIFETIME_MINUTES: i64 = 15;
pub const STELLAR_RESUBMIT_BASE_INTERVAL_SECONDS: i64 = 10;

/// Maximum number of times a Stellar submission may be retried after an insufficient-fee error.
pub const STELLAR_INSUFFICIENT_FEE_MAX_RETRIES: u32 = 2;
/// Raised to cover multiple fast resubmits across short-lived fee spikes.
pub const STELLAR_INSUFFICIENT_FEE_MAX_RETRIES: u32 = 6;

/// Base delay for fast Stellar resubmits, in seconds.
/// Five seconds is approximately one Stellar ledger close time (`STELLAR_LEDGER_TIME_SECONDS`).
pub const STELLAR_FAST_RESUBMIT_BASE_DELAY_SECONDS: i64 = 5;

/// Maximum TRY_AGAIN_LATER rejections that use the fast-resubmit window.
/// After the first three fast attempts, the status-checker backoff ladder owns rescue.
pub const STELLAR_TRY_AGAIN_LATER_FAST_RETRIES: u32 = 3;

/// Maximum resubmit interval (seconds) to cap the resubmission backoff.
/// Prevents excessively long gaps between resubmissions.
Expand Down
655 changes: 620 additions & 35 deletions src/domain/transaction/stellar/submit.rs

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions src/queues/redis/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,8 @@ impl Queue {
.unwrap_or_default();

// Queue configurations:
// - High-frequency: transaction_status (critical path)
// - Low-frequency: request, submission, notifications, health checks, swaps
// - High-frequency: transaction_submission, transaction_status (critical path)
// - Low-frequency: request, notifications, health checks, swaps
let high_frequency = QueueConfig::high_frequency();
let low_frequency = QueueConfig::low_frequency();

Expand All @@ -241,7 +241,9 @@ impl Queue {
transaction_submission_queue: Self::storage(
&format!("{redis_key_prefix}transaction_submission_queue"),
conn_tx_submit,
low_frequency.clone(), // scheduling not used
// Stellar fast resubmit schedules 5s*attempt delayed submit jobs here.
// A 20s sweep quantizes those retries and lets the status checker double-fire.
high_frequency.clone(),
),
transaction_status_queue: Self::storage(
&format!("{redis_key_prefix}transaction_status_queue"),
Expand Down
4 changes: 4 additions & 0 deletions testing/wiremock/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
e2e/env.*
e2e/*.keystore
e2e/**/*.keystore
e2e/generated-relayers/
80 changes: 80 additions & 0 deletions testing/wiremock/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,89 @@ All scenarios fire **exactly once**, then revert to proxying real traffic.
|--------|-------------|
| `trigger-try-again-later.sh` | Arm the TRY_AGAIN_LATER scenario |
| `trigger-insufficient-fee.sh` | Arm the insufficient fee scenario |
| `e2e-fast-resubmit.sh` | Run the Stellar fast-resubmit E2E matrix |
| `check-scenarios.sh` | Show scenario states and recent requests |
| `reset-all.sh` | Reset all scenarios and clear request log |

## E2E fast-resubmit verification

Run the Stellar fast-resubmit matrix from the repo root:

```bash
./testing/wiremock/scripts/e2e-fast-resubmit.sh all
./testing/wiremock/scripts/e2e-fast-resubmit.sh -o /tmp/fast-resubmit-artifacts all
```

Run one scenario by name:

```bash
./testing/wiremock/scripts/e2e-fast-resubmit.sh if-once
./testing/wiremock/scripts/e2e-fast-resubmit.sh if-escalation
./testing/wiremock/scripts/e2e-fast-resubmit.sh if-overcap
./testing/wiremock/scripts/e2e-fast-resubmit.sh tal-window
./testing/wiremock/scripts/e2e-fast-resubmit.sh tal-window-closes
```

The harness verifies the durable regression cases for Stellar fast resubmit on testnet:

| Scenario | Injected responses | Expected behavior |
|----------|--------------------|-------------------|
| `if-once` | 1 insufficient-fee response | One fast retry after about 5s, then submission proceeds |
| `if-escalation` | 3 insufficient-fee responses | Fast retries after about 5s, 10s, and 15s |
| `if-overcap` | 7 insufficient-fee responses | Six fast retries, then failure at the retry cap |
| `tal-window` | 3 TRY_AGAIN_LATER responses | Fast retries after about 5s, 10s, and 15s |
| `tal-window-closes` | 4 TRY_AGAIN_LATER responses | Three fast retries, then status-checker handoff |

The one-shot WireMock mappings remain static. Multi-response chains are registered dynamically by
the runner at scenario arm time through `POST /__admin/mappings`, then deleted before the next
scenario. This keeps the fixture set small while preserving the same Stellar RPC response bodies.

Prerequisites:

- Docker with Compose support.
- Rust/Cargo for `cargo build --bin openzeppelin-relayer`.
- `curl` and `jq`.
- Network access to Stellar testnet, Horizon testnet, and friendbot.
- The local signer keystore is generated automatically at
`testing/wiremock/e2e/local-signer.keystore` if missing, using
`KEYSTORE_PASSPHRASE`, and the generated testnet account is funded through friendbot before
the relayer starts.

The runner starts WireMock, starts a disposable Redis container for the Redis queue backend, builds
and starts `target/debug/openzeppelin-relayer`, funds the configured Stellar testnet account if
needed, runs the selected scenario set, writes artifacts, and tears the stack down.

`if-overcap` runs last in the full matrix because it intentionally never lands on-chain and can
leave the relayer-side local sequence cache ahead of chain state. `tal-window-closes` sends an
explicit 10-minute `valid_until` because it waits for status-checker handoff after the fast retry
window closes.

Artifacts default to `/tmp/oz-relayer-fast-resubmit` and can be overridden with `-o` or
`ARTIFACT_DIR`. The runner writes:

- `verdicts.json`: JSON Lines, one verdict per scenario.
- `relayer.log`: relayer stdout/stderr.
- `harness.log`: stack setup and harness diagnostics.
- `report.md`: line-count summary, deleted/kept scope, and matrix verdicts.

Each verdict line has this schema:

```json
{
"scenario": "if-once",
"send_calls": 2,
"gaps_s": [5],
"final_status": "confirmed",
"fast_log_seen": true,
"checker_log_seen": false,
"pass": true,
"notes": ""
}
```

Gap checks use a tolerance of expected minus 1 second through expected plus 8 seconds.
`tal-window-closes` treats its final checker-rescue gap as `>= 10s`.

## How It Works

WireMock runs as a transparent proxy (`--proxy-all`) to the target RPC endpoint (default: `https://soroban-testnet.stellar.org`). Pre-loaded stub mappings use **scenarios** (state machines) to intercept specific requests:
Expand Down
47 changes: 47 additions & 0 deletions testing/wiremock/e2e/fast-resubmit-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"relayers": [
{
"id": "stellar-fast-resubmit",
"name": "Stellar Fast Resubmit E2E",
"network": "testnet",
"paused": false,
"network_type": "stellar",
"signer_id": "local-signer",
"policies": {
"fee_payment_strategy": "relayer",
"min_balance": 0
}
}
],
"notifications": [],
"signers": [
{
"id": "local-signer",
"type": "local",
"config": {
"path": "testing/wiremock/e2e/local-signer.keystore",
"passphrase": {
"type": "env",
"value": "KEYSTORE_PASSPHRASE"
}
}
}
],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"networks": [
{
"type": "stellar",
"network": "testnet",
"rpc_urls": [
"http://localhost:9090"
],
"explorer_urls": [
"https://stellar.expert/explorer/testnet"
],
"average_blocktime_ms": 5000,
"is_testnet": true,
"passphrase": "Test SDF Network ; September 2015",
"horizon_url": "https://horizon-testnet.stellar.org"
}
],
"plugins": []
}
Loading
Loading