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
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)
- **Rate limiting** (fixed window & token bucket strategies, per-anchor)
- **Request ID propagation** (UUID per flow with tracing)
- 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
- **[RATE_LIMITER.md](./RATE_LIMITER.md)** - Rate limiting for anchor requests
- **[REQUEST_ID_PROPAGATION.md](./REQUEST_ID_PROPAGATION.md)** - Request ID tracking and tracing
- **[API_SPEC.md](./API_SPEC.md)** - API specification and error codes

### Technical Documentation
Expand Down
115 changes: 115 additions & 0 deletions REQUEST_ID_PROPAGATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Request ID Propagation

## Overview

Generate unique request IDs for each flow and propagate across logs and tracing spans.

## Features

- ✅ UUID per flow (128-bit)
- ✅ Visible in tracing spans
- ✅ Automatic timing tracking
- ✅ Success/failure status recording

## Usage

### Generate Request ID

```rust
let request_id = client.generate_request_id();
```

### Submit with Request ID

```rust
let attestation_id = client.submit_with_request_id(
&request_id,
&issuer,
&subject,
&timestamp,
&payload_hash,
&signature,
);
```

### Retrieve Tracing Span

```rust
let span = client.get_tracing_span(&request_id.id);

println!("Operation: {}", span.operation);
println!("Actor: {}", span.actor);
println!("Status: {}", span.status);
println!("Duration: {} seconds", span.completed_at - span.started_at);
```

## API Methods

```rust
// Generate unique request ID
pub fn generate_request_id() -> RequestId

// Submit attestation with tracking
pub fn submit_with_request_id(
request_id: RequestId,
issuer: Address,
subject: Address,
timestamp: u64,
payload_hash: BytesN<32>,
signature: Bytes,
) -> Result<u64, Error>

// Submit quote with tracking
pub fn quote_with_request_id(
request_id: RequestId,
anchor: Address,
base_asset: String,
quote_asset: String,
rate: u64,
fee_percentage: u32,
minimum_amount: u64,
maximum_amount: u64,
valid_until: u64,
) -> Result<u64, Error>

// Get tracing span
pub fn get_tracing_span(request_id: BytesN<16>) -> Option<TracingSpan>
```

## Tracing Span Structure

```rust
pub struct TracingSpan {
pub request_id: RequestId,
pub operation: String, // Operation name
pub actor: Address, // Who performed it
pub started_at: u64, // Start timestamp
pub completed_at: u64, // End timestamp
pub status: String, // "success" or "failed"
}
```

## Use Cases

### Debugging
Track request flow through system for troubleshooting.

### Performance Monitoring
Measure operation duration via `completed_at - started_at`.

### Audit Trail
Complete record of who did what and when.

### Distributed Tracing
Correlate operations across multiple calls.

## Storage

Tracing spans stored in temporary storage with 1-day TTL.

## Best Practices

1. **Generate once per flow** - Reuse same request ID for related operations
2. **Check span status** - Verify success/failure in tracing data
3. **Monitor timing** - Track performance via span timestamps
4. **Log request IDs** - Include in external logs for correlation
123 changes: 120 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ mod credentials;
mod error_mapping;
mod errors;
mod events;
mod rate_limiter;
mod request_id;
mod retry;
mod serialization;
mod storage;
Expand Down Expand Up @@ -54,7 +54,7 @@ mod cross_platform_tests;
mod zerocopy_tests;

#[cfg(test)]
mod rate_limiter_tests;
mod request_id_tests;


use soroban_sdk::{contract, contractimpl, Address, Bytes, BytesN, Env, String, Vec};
Expand All @@ -67,7 +67,7 @@ pub use events::{
OperationLogged, QuoteReceived, QuoteSubmitted, ServicesConfigured, SessionCreated,
SettlementConfirmed, TransferInitiated,
};
pub use rate_limiter::{RateLimitConfig, RateLimitStrategy, RateLimiter};
pub use request_id::{RequestId, RequestTracker, TracingSpan};
pub use storage::Storage;
pub use types::{
AnchorMetadata, AnchorOption, AnchorServices, Attestation, AuditLog, Endpoint, HealthStatus,
Expand Down Expand Up @@ -1199,4 +1199,121 @@ impl AnchorKitContract {

Ok(())
}

// ============ Request ID Propagation ============

/// Generate a unique request ID for flow tracking.
pub fn generate_request_id(env: Env) -> RequestId {
RequestId::generate(&env)
}

/// Submit attestation with request ID tracking.
pub fn submit_with_request_id(
env: Env,
request_id: RequestId,
issuer: Address,
subject: Address,
timestamp: u64,
payload_hash: BytesN<32>,
signature: Bytes,
) -> Result<u64, Error> {
issuer.require_auth();

let start_time = env.ledger().timestamp();

// Perform attestation
let result = if timestamp == 0 {
Err(Error::InvalidTimestamp)
} else if !Storage::is_attestor(&env, &issuer) {
Err(Error::UnauthorizedAttestor)
} else if Storage::is_hash_used(&env, &payload_hash) {
Err(Error::ReplayAttack)
} else {
Self::verify_signature(&env, &issuer, &subject, timestamp, &payload_hash, &signature)?;

let id = Storage::get_and_increment_counter(&env);
let attestation = Attestation {
id,
issuer: issuer.clone(),
subject: subject.clone(),
timestamp,
payload_hash: payload_hash.clone(),
signature,
};

Storage::set_attestation(&env, id, &attestation);
Storage::mark_hash_used(&env, &payload_hash);
AttestationRecorded::publish(&env, id, &subject, timestamp, payload_hash);

Ok(id)
};

// Store tracing span
let span = TracingSpan {
request_id: request_id.clone(),
operation: soroban_sdk::String::from_str(&env, "submit_attestation"),
actor: issuer,
started_at: start_time,
completed_at: env.ledger().timestamp(),
status: if result.is_ok() {
soroban_sdk::String::from_str(&env, "success")
} else {
soroban_sdk::String::from_str(&env, "failed")
},
};
RequestTracker::store_span(&env, &span);

result
}

/// Get tracing span by request ID.
pub fn get_tracing_span(env: Env, request_id: BytesN<16>) -> Option<TracingSpan> {
RequestTracker::get_span(&env, &request_id)
}

/// Submit quote with request ID tracking.
pub fn quote_with_request_id(
env: Env,
request_id: RequestId,
anchor: Address,
base_asset: soroban_sdk::String,
quote_asset: soroban_sdk::String,
rate: u64,
fee_percentage: u32,
minimum_amount: u64,
maximum_amount: u64,
valid_until: u64,
) -> Result<u64, Error> {
anchor.require_auth();

let start_time = env.ledger().timestamp();

let result = Self::submit_quote(
env.clone(),
anchor.clone(),
base_asset,
quote_asset,
rate,
fee_percentage,
minimum_amount,
maximum_amount,
valid_until,
);

let span = TracingSpan {
request_id: request_id.clone(),
operation: soroban_sdk::String::from_str(&env, "submit_quote"),
actor: anchor,
started_at: start_time,
completed_at: env.ledger().timestamp(),
status: if result.is_ok() {
soroban_sdk::String::from_str(&env, "success")
} else {
soroban_sdk::String::from_str(&env, "failed")
},
};
RequestTracker::store_span(&env, &span);

result
}
}
64 changes: 64 additions & 0 deletions src/request_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use soroban_sdk::{contracttype, Bytes, BytesN, Env};

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RequestId {
pub id: BytesN<16>, // 128-bit UUID
pub created_at: u64,
}

impl RequestId {
pub fn generate(env: &Env) -> Self {
let timestamp = env.ledger().timestamp();
let sequence = env.ledger().sequence();

// Generate pseudo-UUID from timestamp + sequence
let mut bytes = Bytes::new(env);
bytes.append(&Bytes::from_array(env, &timestamp.to_be_bytes()));
bytes.append(&Bytes::from_array(env, &sequence.to_be_bytes()));

// Hash to get 32 bytes, take first 16
let hash = env.crypto().sha256(&bytes);
let hash_bytes = hash.to_array();
let id = BytesN::from_array(env, &[
hash_bytes[0], hash_bytes[1], hash_bytes[2], hash_bytes[3],
hash_bytes[4], hash_bytes[5], hash_bytes[6], hash_bytes[7],
hash_bytes[8], hash_bytes[9], hash_bytes[10], hash_bytes[11],
hash_bytes[12], hash_bytes[13], hash_bytes[14], hash_bytes[15],
]);

Self {
id,
created_at: timestamp,
}
}
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TracingSpan {
pub request_id: RequestId,
pub operation: soroban_sdk::String,
pub actor: soroban_sdk::Address,
pub started_at: u64,
pub completed_at: u64,
pub status: soroban_sdk::String,
}

pub struct RequestTracker;

impl RequestTracker {
pub fn store_span(env: &Env, span: &TracingSpan) {
let key = (
soroban_sdk::symbol_short!("SPAN"),
span.request_id.id.clone(),
);
env.storage().temporary().set(&key, span);
env.storage().temporary().extend_ttl(&key, 17280, 17280); // 1 day
}

pub fn get_span(env: &Env, request_id: &BytesN<16>) -> Option<TracingSpan> {
let key = (soroban_sdk::symbol_short!("SPAN"), request_id.clone());
env.storage().temporary().get(&key)
}
}
Loading
Loading