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
88 changes: 82 additions & 6 deletions contracts/teachlink/src/bft_consensus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,15 @@ use crate::storage::{
VALIDATORS, VALIDATOR_ACTIVITY_SEQ, VALIDATOR_INFO, VALIDATOR_STAKES,
};
use crate::types::{
BridgeProposal, ConsensusState, CrossChainMessage, ProposalStatus, ValidatorInfo,
BridgeProposal, ConsensusState, CrossChainMessage, NetworkCondition, NetworkHealth,
ProposalStatus, ValidatorInfo,
};
use soroban_sdk::{Address, Env, Map, Vec};

/// Minimum stake required to become a validator
pub const MIN_VALIDATOR_STAKE: i128 = 100_000_000; // 100 tokens with 6 decimals

/// Proposal timeout in seconds (24 hours)
/// Proposal timeout in seconds (24 hours) – base value for healthy network.
pub const PROPOSAL_TIMEOUT: u64 = 86_400;

/// Number of consensus rounds per rotation epoch.
Expand All @@ -79,6 +80,69 @@ pub const MIN_ACTIVE_REPUTATION: u32 = 40;
pub struct BFTConsensus;

impl BFTConsensus {
// ── Network condition monitoring ─────────────────────────────────────────

/// Return the current network condition, defaulting to Healthy if not set.
pub fn get_network_condition(env: &Env) -> NetworkCondition {
env.storage()
.instance()
.get(&NETWORK_STATE)
.unwrap_or(NetworkCondition {
health: NetworkHealth::Healthy,
avg_latency_ms: 0,
consecutive_misses: 0,
last_updated: env.ledger().timestamp(),
})
}

/// Update network condition from an external observer (admin / oracle).
///
/// Derives `health` automatically from `consecutive_misses`:
/// - ≥ `MISS_THRESHOLD_CRITICAL` → Critical
/// - ≥ `MISS_THRESHOLD_DEGRADED` → Degraded
/// - otherwise → Healthy
pub fn update_network_condition(
env: &Env,
avg_latency_ms: u64,
consecutive_misses: u32,
) -> NetworkCondition {
let health = if consecutive_misses >= MISS_THRESHOLD_CRITICAL {
NetworkHealth::Critical
} else if consecutive_misses >= MISS_THRESHOLD_DEGRADED {
NetworkHealth::Degraded
} else {
NetworkHealth::Healthy
};

let condition = NetworkCondition {
health,
avg_latency_ms,
consecutive_misses,
last_updated: env.ledger().timestamp(),
};
env.storage().instance().set(&NETWORK_STATE, &condition);
condition
}

/// Compute the adaptive proposal timeout based on current network health.
///
/// | Health | Multiplier | Effective timeout |
/// |----------|-----------|-------------------|
/// | Healthy | 1× | 86 400 s (24 h) |
/// | Degraded | 2× | 172 800 s (48 h) |
/// | Critical | 3× | 259 200 s (72 h) |
pub fn adaptive_timeout(env: &Env) -> u64 {
let condition = Self::get_network_condition(env);
let multiplier = match condition.health {
NetworkHealth::Healthy => 1,
NetworkHealth::Degraded => TIMEOUT_MULTIPLIER_DEGRADED,
NetworkHealth::Critical => TIMEOUT_MULTIPLIER_CRITICAL,
};
PROPOSAL_TIMEOUT.saturating_mul(multiplier)
}

// ── Validator registration ───────────────────────────────────────────────

/// Register a new validator with stake
pub fn register_validator(
env: &Env,
Expand Down Expand Up @@ -262,6 +326,9 @@ impl BFTConsensus {

let required_votes = consensus_state.byzantine_threshold;

// Compute timeout based on current network health (graceful degradation).
let timeout = Self::adaptive_timeout(env);

// Create proposal
let proposal = BridgeProposal {
proposal_id: proposal_counter,
Expand All @@ -271,7 +338,7 @@ impl BFTConsensus {
required_votes,
status: ProposalStatus::Pending,
created_at: env.ledger().timestamp(),
expires_at: env.ledger().timestamp() + PROPOSAL_TIMEOUT,
expires_at: env.ledger().timestamp() + timeout,
};

// Store proposal
Expand All @@ -290,9 +357,7 @@ impl BFTConsensus {
let expires_seq =
env.ledger()
.sequence()
.saturating_add(crate::ledger_time::seconds_to_ledger_delta(
PROPOSAL_TIMEOUT,
));
.saturating_add(crate::ledger_time::seconds_to_ledger_delta(timeout));
let mut proposal_expires_seq: Map<u64, u32> = env
.storage()
.instance()
Expand Down Expand Up @@ -381,6 +446,13 @@ impl BFTConsensus {
proposal.status = ProposalStatus::Expired;
proposals.set(proposal_id, proposal);
env.storage().instance().set(&BRIDGE_PROPOSALS, &proposals);
// Record the miss so the adaptive monitor can react.
let cond = Self::get_network_condition(env);
Self::update_network_condition(
env,
cond.avg_latency_ms,
cond.consecutive_misses.saturating_add(1),
);
return Err(BridgeError::ProposalExpired);
}

Expand Down Expand Up @@ -450,6 +522,10 @@ impl BFTConsensus {
.instance()
.set(&CONSENSUS_STATE, &consensus_state);

// Successful consensus – reset consecutive miss counter.
let cond = Self::get_network_condition(env);
Self::update_network_condition(env, cond.avg_latency_ms, 0);

// Emit event
ProposalExecutedEvent {
proposal_id,
Expand Down
1 change: 1 addition & 0 deletions contracts/teachlink/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ pub const BRIDGE_PROPOSALS: Symbol = symbol_short!("proposals");
pub const PROPOSAL_COUNTER: Symbol = symbol_short!("prop_cnt");
pub const CONSENSUS_STATE: Symbol = symbol_short!("cons_st");
pub const VALIDATOR_STAKES: Symbol = symbol_short!("val_stake");
pub const NETWORK_STATE: Symbol = symbol_short!("net_state");

// Slashing and Rewards Storage
pub const SLASHING_RECORDS: Symbol = symbol_short!("slash_rec");
Expand Down
26 changes: 26 additions & 0 deletions contracts/teachlink/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,32 @@ pub struct ChainAssetInfo {

// ========== BFT Consensus Types ==========

/// Network health level used to drive adaptive consensus timeouts.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum NetworkHealth {
/// Normal operation – use base timeout.
Healthy,
/// Elevated latency – apply moderate multiplier.
Degraded,
/// Severe degradation – apply maximum multiplier (graceful degradation mode).
Critical,
}

/// Snapshot of observed network conditions stored on-chain.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NetworkCondition {
/// Current health classification.
pub health: NetworkHealth,
/// Rolling average round-trip latency in milliseconds (0 if unknown).
pub avg_latency_ms: u64,
/// Consecutive rounds where quorum was not reached in time (miss counter).
pub consecutive_misses: u32,
/// Ledger timestamp of the last update.
pub last_updated: u64,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ValidatorInfo {
Expand Down
Loading