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
157 changes: 157 additions & 0 deletions FALLBACK_SELECTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Fallback Anchor Selection

## Overview

Automatically reroute to fallback anchors when preferred anchor fails.

## Features

- ✅ Configurable fallback order
- ✅ Failure detection logic
- ✅ Automatic anchor health tracking
- ✅ Retry with max attempts

## Usage

### Configure Fallback Order

```rust
let anchor_order = vec![&env, anchor1, anchor2, anchor3];
client.configure_fallback(
&anchor_order,
&3, // max_retries
&2 // failure_threshold
);
```

### Record Failures

```rust
// Automatically tracked, or manually:
client.record_anchor_failure(&anchor);
```

### Record Success

```rust
client.record_anchor_success(&anchor);
```

### Select Next Anchor

```rust
// Get next available anchor
let next_anchor = client.select_fallback_anchor(&Some(failed_anchor));

// Or start from beginning
let first_anchor = client.select_fallback_anchor(&None);
```

### Automatic Fallback

```rust
// Automatically tries fallback anchors on failure
let quote_id = client.submit_quote_with_fallback(
&base_asset,
&quote_asset,
&rate,
&fee_percentage,
&minimum_amount,
&maximum_amount,
&valid_until,
);
```

## API Methods

```rust
// Configure fallback
pub fn configure_fallback(
anchor_order: Vec<Address>,
max_retries: u32,
failure_threshold: u32,
) -> Result<(), Error>

// Get configuration
pub fn get_fallback_config() -> Option<FallbackConfig>

// Record anchor state
pub fn record_anchor_failure(anchor: Address) -> Result<(), Error>
pub fn record_anchor_success(anchor: Address) -> Result<(), Error>

// Get failure state
pub fn get_anchor_failure_state(anchor: Address) -> Option<AnchorFailureState>

// Select fallback
pub fn select_fallback_anchor(failed_anchor: Option<Address>) -> Result<Address, Error>

// Automatic fallback
pub fn submit_quote_with_fallback(...) -> Result<u64, Error>
```

## Configuration

```rust
pub struct FallbackConfig {
pub anchor_order: Vec<Address>, // Ordered list to try
pub max_retries: u32, // Max retry attempts
pub failure_threshold: u32, // Failures before marking down
}
```

## Failure State

```rust
pub struct AnchorFailureState {
pub anchor: Address,
pub failure_count: u32,
pub last_failure: u64,
pub is_down: bool, // true when >= threshold
}
```

## How It Works

1. **Configure Order**: Set preferred anchor order
2. **Detect Failure**: Track failures per anchor
3. **Mark Down**: After threshold failures, mark anchor as down
4. **Skip Down Anchors**: Automatically skip unavailable anchors
5. **Retry**: Try next anchor in order
6. **Success Clears**: Success resets failure state

## Example Flow

```rust
// Setup
let order = vec![&env, primary, secondary, tertiary];
client.configure_fallback(&order, &3, &2);

// Primary fails twice - marked down
client.record_anchor_failure(&primary);
client.record_anchor_failure(&primary);

// Next selection skips primary, returns secondary
let next = client.select_fallback_anchor(&None);
assert_eq!(next, secondary);

// Secondary succeeds - clears its failure state
client.record_anchor_success(&secondary);
```

## Storage

- **Config**: Persistent storage (90-day TTL)
- **Failure State**: Temporary storage (1-day TTL)

## Best Practices

1. **Order by preference** - Put most reliable anchors first
2. **Set appropriate threshold** - Balance sensitivity vs false positives
3. **Monitor failure states** - Track which anchors are down
4. **Clear on success** - Automatically done by `record_anchor_success`
5. **Use automatic fallback** - Let system handle retries

## Error Handling

- `Error::NoAnchorsAvailable` - All anchors down or exhausted retries
- `Error::InvalidConfig` - No fallback config set
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ AnchorKit is a Soroban-native toolkit for anchoring off-chain attestations to St
- Endpoint configuration for attestors
- Service capability discovery (deposits, withdrawals, quotes, KYC)
- **Health monitoring** (latency, failures, availability)
- **Request ID propagation** (UUID per flow with tracing)
- **Fallback anchor selection** (automatic rerouting on failure)
- Event emission for all state changes
- Comprehensive error handling with stable error codes

Expand Down Expand Up @@ -102,7 +102,7 @@ const auditLog = await contract.get_audit_log(0);
- **[SESSION_TRACEABILITY.md](./SESSION_TRACEABILITY.md)** - Complete feature guide with usage patterns
- **[SECURE_CREDENTIALS.md](./SECURE_CREDENTIALS.md)** - Secure credential injection and management
- **[HEALTH_MONITORING.md](./HEALTH_MONITORING.md)** - Anchor health monitoring interface
- **[REQUEST_ID_PROPAGATION.md](./REQUEST_ID_PROPAGATION.md)** - Request ID tracking and tracing
- **[FALLBACK_SELECTION.md](./FALLBACK_SELECTION.md)** - Automatic fallback anchor selection
- **[API_SPEC.md](./API_SPEC.md)** - API specification and error codes

### Technical Documentation
Expand Down
105 changes: 105 additions & 0 deletions src/fallback.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use soroban_sdk::{contracttype, Address, Env, Vec};

use crate::{types::QuoteData, Error};

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FallbackConfig {
pub anchor_order: Vec<Address>, // Ordered list of anchors to try
pub max_retries: u32,
pub failure_threshold: u32, // Failures before marking anchor as down
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AnchorFailureState {
pub anchor: Address,
pub failure_count: u32,
pub last_failure: u64,
pub is_down: bool,
}

pub struct FallbackSelector;

impl FallbackSelector {
pub fn set_config(env: &Env, config: &FallbackConfig) {
let key = soroban_sdk::symbol_short!("FBCONFIG");
env.storage().persistent().set(&key, config);
env.storage().persistent().extend_ttl(&key, 7776000, 7776000); // 90 days
}

pub fn get_config(env: &Env) -> Option<FallbackConfig> {
let key = soroban_sdk::symbol_short!("FBCONFIG");
env.storage().persistent().get(&key)
}

pub fn record_failure(env: &Env, anchor: &Address, threshold: u32) {
let key = (soroban_sdk::symbol_short!("FBFAIL"), anchor);
let mut state: AnchorFailureState = env
.storage()
.temporary()
.get(&key)
.unwrap_or(AnchorFailureState {
anchor: anchor.clone(),
failure_count: 0,
last_failure: 0,
is_down: false,
});

state.failure_count += 1;
state.last_failure = env.ledger().timestamp();
state.is_down = state.failure_count >= threshold;

env.storage().temporary().set(&key, &state);
env.storage().temporary().extend_ttl(&key, 17280, 17280); // 1 day
}

pub fn record_success(env: &Env, anchor: &Address) {
let key = (soroban_sdk::symbol_short!("FBFAIL"), anchor);
env.storage().temporary().remove(&key);
}

pub fn get_failure_state(env: &Env, anchor: &Address) -> Option<AnchorFailureState> {
let key = (soroban_sdk::symbol_short!("FBFAIL"), anchor);
env.storage().temporary().get(&key)
}

pub fn is_anchor_available(env: &Env, anchor: &Address) -> bool {
if let Some(state) = Self::get_failure_state(env, anchor) {
!state.is_down
} else {
true
}
}

pub fn select_next_anchor(
env: &Env,
config: &FallbackConfig,
failed_anchor: Option<&Address>,
) -> Result<Address, Error> {
let mut start_index = 0;

// Find where to start in the fallback order
if let Some(failed) = failed_anchor {
for i in 0..config.anchor_order.len() {
if let Some(addr) = config.anchor_order.get(i) {
if addr == *failed {
start_index = i + 1;
break;
}
}
}
}

// Try anchors in order
for i in start_index..config.anchor_order.len() {
if let Some(anchor) = config.anchor_order.get(i) {
if Self::is_anchor_available(env, &anchor) {
return Ok(anchor);
}
}
}

Err(Error::NoAnchorsAvailable)
}
}
Loading
Loading