From ea1b2f162b68f777baea0c34a7da0a3ae02e689f Mon Sep 17 00:00:00 2001 From: ABresting Date: Fri, 13 Feb 2026 22:50:14 +0100 Subject: [PATCH 01/12] pocw: add PoCWConfig with enabled flag (#8) --- types/Cargo.toml | 3 ++ types/src/consensus.rs | 3 ++ types/src/lib.rs | 3 ++ types/src/pocw.rs | 81 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 types/src/pocw.rs diff --git a/types/Cargo.toml b/types/Cargo.toml index 528ef16..cbbfcd5 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -16,3 +16,6 @@ bincode = "2.0" bcs = "0.1" setu-vlc = { path = "../crates/setu-vlc" } ed25519-dalek = { version = "2.1", features = ["serde"] } + +[dev-dependencies] +serde_json = "1.0" diff --git a/types/src/consensus.rs b/types/src/consensus.rs index ab3bc14..68de255 100644 --- a/types/src/consensus.rs +++ b/types/src/consensus.rs @@ -384,6 +384,8 @@ pub struct ConsensusConfig { pub max_events_per_cf: usize, pub cf_timeout_ms: u64, pub validator_count: usize, + #[serde(default)] + pub pocw: Option, } impl Default for ConsensusConfig { @@ -394,6 +396,7 @@ impl Default for ConsensusConfig { max_events_per_cf: 1000, cf_timeout_ms: 5000, validator_count: 3, + pocw: None, } } } diff --git a/types/src/lib.rs b/types/src/lib.rs index dd66bcd..00995ea 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -8,6 +8,7 @@ pub mod object; pub mod subnet; // Subnet (sub-application) types pub mod merkle; // Merkle tree types for state commitment pub mod task; // Task types for Validator → Solver communication +pub mod pocw; // PoCW economic types // ========== Object Model ========== pub mod coin; // Coin object (transferable asset) @@ -80,6 +81,8 @@ pub use merkle::{ pub use account_view::AccountView; // Task types for Validator → Solver communication +pub use pocw::PoCWConfig; + pub use task::{ SolverTask, ResolvedInputs, OperationType, ResolvedObject, ReadSetEntry, MerkleProof, diff --git a/types/src/pocw.rs b/types/src/pocw.rs new file mode 100644 index 0000000..2403e18 --- /dev/null +++ b/types/src/pocw.rs @@ -0,0 +1,81 @@ +//! PoCW (Proof of Causal Work) and Flux economic types for FluxTransfer +//! +//! Defines configuration and economic structures for the FluxTransfer path: +//! fixed burn fee, flat power drain, and optional nominal solver reward. + +use serde::{Deserialize, Serialize}; + +/// Economic configuration for the Flux system. +/// +/// Controls burn fees, power drain, and solver rewards for FluxTransfer transactions. +/// Use `enabled` to toggle the entire economic subsystem at runtime. +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct PoCWConfig { + /// Master toggle for PoCW economics + pub enabled: bool, + /// Fixed gas fee for FluxTransfer transactions + pub transfer_fixed_fee: u64, + /// Flat power drain per FluxTransfer + pub transfer_power_drain: u64, + /// Whether solvers receive a nominal reward for processing FluxTransfer transactions + pub solver_transfer_reward_enabled: bool, + /// Nominal Flux reward per FluxTransfer processed by a solver + pub solver_transfer_reward: u64, +} + +impl Default for PoCWConfig { + fn default() -> Self { + Self { + enabled: false, + transfer_fixed_fee: 21_000, + transfer_power_drain: 1, + solver_transfer_reward_enabled: false, + solver_transfer_reward: 1, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_defaults() { + let config = PoCWConfig::default(); + assert!(!config.enabled); + assert_eq!(config.transfer_fixed_fee, 21_000); + assert_eq!(config.transfer_power_drain, 1); + assert!(!config.solver_transfer_reward_enabled); + assert_eq!(config.solver_transfer_reward, 1); + } + + #[test] + fn test_serde_roundtrip() { + let config = PoCWConfig::default(); + let json = serde_json::to_string(&config).unwrap(); + let deserialized: PoCWConfig = serde_json::from_str(&json).unwrap(); + + assert_eq!(config.enabled, deserialized.enabled); + assert_eq!(config.transfer_fixed_fee, deserialized.transfer_fixed_fee); + assert_eq!(config.transfer_power_drain, deserialized.transfer_power_drain); + assert_eq!(config.solver_transfer_reward_enabled, deserialized.solver_transfer_reward_enabled); + assert_eq!(config.solver_transfer_reward, deserialized.solver_transfer_reward); + } + + #[test] + fn test_custom_config() { + let config = PoCWConfig { + enabled: true, + solver_transfer_reward_enabled: true, + solver_transfer_reward: 5, + transfer_fixed_fee: 10_000, + ..Default::default() + }; + + assert!(config.enabled); + assert!(config.solver_transfer_reward_enabled); + assert_eq!(config.solver_transfer_reward, 5); + assert_eq!(config.transfer_fixed_fee, 10_000); + assert_eq!(config.transfer_power_drain, 1); + } +} From bdad62a5d79ae07a29df1ac4586df4323e140864 Mon Sep 17 00:00:00 2001 From: ABresting Date: Fri, 13 Feb 2026 22:56:07 +0100 Subject: [PATCH 02/12] pocw: add executed_by field to Event (#8) --- types/src/event.rs | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/types/src/event.rs b/types/src/event.rs index 4a2d678..832e29e 100644 --- a/types/src/event.rs +++ b/types/src/event.rs @@ -255,9 +255,13 @@ pub struct Event { /// Execution result (if executed) #[serde(default)] pub execution_result: Option, - + /// Creation timestamp (milliseconds since epoch) pub timestamp: u64, + + /// Solver that executed this event (creator is the validator, not the solver) + #[serde(default)] + pub executed_by: Option, } impl Event { @@ -294,6 +298,7 @@ impl Event { status: EventStatus::Pending, execution_result: None, timestamp, + executed_by: None, } } @@ -715,4 +720,33 @@ mod tests { assert!(result.success); assert_eq!(result.state_changes.len(), 1); } + + #[test] + fn test_executed_by_default_none() { + let event = Event::new( + EventType::Transfer, + vec![], + create_vlc_snapshot(), + "node1".to_string(), + ); + assert!(event.executed_by.is_none()); + } + + #[test] + fn test_event_backward_compat_serde() { + // JSON without executed_by should deserialize with None default + let json = r#"{ + "id": "abc123", + "event_type": "Transfer", + "parent_ids": [], + "payload": "None", + "vlc_snapshot": {"vector_clock": {"clocks": {}}, "logical_time": 1, "physical_time": 1000}, + "creator": "node1", + "status": "Pending", + "timestamp": 1000 + }"#; + + let event: Event = serde_json::from_str(json).unwrap(); + assert!(event.executed_by.is_none()); + } } From d5ae7f106b06d1e2c8bad3434b68ba9112513fd2 Mon Sep 17 00:00:00 2001 From: ABresting Date: Fri, 13 Feb 2026 23:13:05 +0100 Subject: [PATCH 03/12] pocw: add flux burn and power drain for transfers (#8) --- consensus/src/engine.rs | 4 +++ consensus/src/lib.rs | 1 + consensus/src/pocw/flux_burn.rs | 44 +++++++++++++++++++++++++++++++++ consensus/src/pocw/mod.rs | 6 +++++ consensus/src/pocw/power.rs | 44 +++++++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+) create mode 100644 consensus/src/pocw/flux_burn.rs create mode 100644 consensus/src/pocw/mod.rs create mode 100644 consensus/src/pocw/power.rs diff --git a/consensus/src/engine.rs b/consensus/src/engine.rs index f8f86e1..f2ade43 100644 --- a/consensus/src/engine.rs +++ b/consensus/src/engine.rs @@ -1390,6 +1390,7 @@ mod tests { max_events_per_cf: 100, cf_timeout_ms: 5000, validator_count: 4, // 4 validators: need 2 rejects to reject (1/3+1) + pocw: None, }; let validator_set = create_validator_set(); @@ -1446,6 +1447,7 @@ mod tests { max_events_per_cf: 100, cf_timeout_ms: 100, // 100ms timeout for testing validator_count: 4, + pocw: None, }; let validator_set = create_validator_set(); @@ -1500,6 +1502,7 @@ mod tests { max_events_per_cf: 100, cf_timeout_ms: 100, // 100ms timeout validator_count: 4, + pocw: None, }; let validator_set = create_validator_set(); @@ -1549,6 +1552,7 @@ mod tests { max_events_per_cf: 100, cf_timeout_ms: 5000, validator_count: 4, + pocw: None, }; let validator_set = create_validator_set(); diff --git a/consensus/src/lib.rs b/consensus/src/lib.rs index 229974b..d69d92b 100644 --- a/consensus/src/lib.rs +++ b/consensus/src/lib.rs @@ -45,6 +45,7 @@ pub mod router; pub mod tee_verifier; pub mod validator_set; pub mod vlc; +pub mod pocw; // Re-export main types pub use anchor_builder::{ diff --git a/consensus/src/pocw/flux_burn.rs b/consensus/src/pocw/flux_burn.rs new file mode 100644 index 0000000..498929b --- /dev/null +++ b/consensus/src/pocw/flux_burn.rs @@ -0,0 +1,44 @@ +//! Flux burn calculation for FluxTransfer transactions. +//! +//! Returns the configured fixed fee when PoCW is enabled, 0 otherwise. + +use setu_types::pocw::PoCWConfig; + +/// Calculate the Flux burn for a FluxTransfer transaction. +pub fn calculate_transfer_burn(config: &PoCWConfig) -> u64 { + if config.enabled { + config.transfer_fixed_fee + } else { + 0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_enabled_returns_fixed_fee() { + let config = PoCWConfig { + enabled: true, + ..Default::default() + }; + assert_eq!(calculate_transfer_burn(&config), 21_000); + } + + #[test] + fn test_disabled_returns_zero() { + let config = PoCWConfig::default(); // enabled: false + assert_eq!(calculate_transfer_burn(&config), 0); + } + + #[test] + fn test_custom_fee() { + let config = PoCWConfig { + enabled: true, + transfer_fixed_fee: 50_000, + ..Default::default() + }; + assert_eq!(calculate_transfer_burn(&config), 50_000); + } +} diff --git a/consensus/src/pocw/mod.rs b/consensus/src/pocw/mod.rs new file mode 100644 index 0000000..7104e27 --- /dev/null +++ b/consensus/src/pocw/mod.rs @@ -0,0 +1,6 @@ +//! PoCW economic calculations for FluxTransfer transactions +//! +//! Provides flux burn and power drain computation, gated by PoCWConfig::enabled. + +pub mod flux_burn; +pub mod power; diff --git a/consensus/src/pocw/power.rs b/consensus/src/pocw/power.rs new file mode 100644 index 0000000..141903b --- /dev/null +++ b/consensus/src/pocw/power.rs @@ -0,0 +1,44 @@ +//! Power drain calculation for FluxTransfer transactions. +//! +//! Returns the configured flat drain when PoCW is enabled, 0 otherwise. + +use setu_types::pocw::PoCWConfig; + +/// Calculate the power drain for a FluxTransfer transaction. +pub fn calculate_transfer_power_drain(config: &PoCWConfig) -> u64 { + if config.enabled { + config.transfer_power_drain + } else { + 0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_enabled_returns_flat_drain() { + let config = PoCWConfig { + enabled: true, + ..Default::default() + }; + assert_eq!(calculate_transfer_power_drain(&config), 1); + } + + #[test] + fn test_disabled_returns_zero() { + let config = PoCWConfig::default(); // enabled: false + assert_eq!(calculate_transfer_power_drain(&config), 0); + } + + #[test] + fn test_custom_drain() { + let config = PoCWConfig { + enabled: true, + transfer_power_drain: 10, + ..Default::default() + }; + assert_eq!(calculate_transfer_power_drain(&config), 10); + } +} From 0332b2ee0ee2fe11a17d48a14988ddb9122bddb6 Mon Sep 17 00:00:00 2001 From: ABresting Date: Fri, 13 Feb 2026 23:35:48 +0100 Subject: [PATCH 04/12] pocw: add fold processor with solver rewards (#8) --- consensus/src/pocw/mod.rs | 1 + consensus/src/pocw/processor.rs | 209 ++++++++++++++++++++++++++++++++ types/src/lib.rs | 2 +- types/src/pocw.rs | 25 ++++ 4 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 consensus/src/pocw/processor.rs diff --git a/consensus/src/pocw/mod.rs b/consensus/src/pocw/mod.rs index 7104e27..c1bea3e 100644 --- a/consensus/src/pocw/mod.rs +++ b/consensus/src/pocw/mod.rs @@ -4,3 +4,4 @@ pub mod flux_burn; pub mod power; +pub mod processor; diff --git a/consensus/src/pocw/processor.rs b/consensus/src/pocw/processor.rs new file mode 100644 index 0000000..f9e0d75 --- /dev/null +++ b/consensus/src/pocw/processor.rs @@ -0,0 +1,209 @@ +//! Fold-level economic processor for FluxTransfer transactions. +//! +//! Takes a set of fold events and a PoCWConfig, produces a FoldEconomics summary +//! with per-solver reward breakdown. + +use std::collections::HashMap; +use setu_types::event::EventType; +use setu_types::pocw::{PoCWConfig, FoldEconomics, SolverReward}; +use setu_types::Event; + +use super::flux_burn::calculate_transfer_burn; +use super::power::calculate_transfer_power_drain; + +/// Process a fold's events and produce an economic summary. +/// +/// Only FluxTransfer events with `executed_by` set are counted for solver rewards. +/// Returns `None` if PoCW is disabled. +pub fn process_fold(config: &PoCWConfig, events: &[Event]) -> Option { + if !config.enabled { + return None; + } + + let burn_per_transfer = calculate_transfer_burn(config); + let power_per_transfer = calculate_transfer_power_drain(config); + + let mut transfer_count: usize = 0; + let mut solver_transfers: HashMap = HashMap::new(); + + for event in events { + if event.event_type != EventType::Transfer { + continue; + } + transfer_count += 1; + + if let Some(solver_id) = &event.executed_by { + *solver_transfers.entry(solver_id.clone()).or_default() += 1; + } + } + + let total_flux_burned = burn_per_transfer * transfer_count as u64; + let total_power_consumed = power_per_transfer * transfer_count as u64; + + let mut solver_rewards = Vec::new(); + let mut total_solver_rewards: u64 = 0; + + if config.solver_transfer_reward_enabled { + for (solver_id, count) in &solver_transfers { + let reward = config.solver_transfer_reward * count; + total_solver_rewards += reward; + solver_rewards.push(SolverReward { + solver_id: solver_id.clone(), + transfer_count: *count, + flux_reward: reward, + }); + } + solver_rewards.sort_by(|a, b| a.solver_id.cmp(&b.solver_id)); + } + + Some(FoldEconomics { + event_count: events.len(), + total_flux_burned, + total_power_consumed, + total_solver_rewards, + solver_rewards, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use setu_types::event::VLCSnapshot; + + fn make_transfer_event(executed_by: Option<&str>) -> Event { + let mut event = Event::new( + EventType::Transfer, + vec![], + VLCSnapshot::new(), + "validator-1".to_string(), + ); + event.executed_by = executed_by.map(|s| s.to_string()); + event + } + + fn make_genesis_event() -> Event { + Event::new( + EventType::Genesis, + vec![], + VLCSnapshot::new(), + "validator-1".to_string(), + ) + } + + fn enabled_config() -> PoCWConfig { + PoCWConfig { + enabled: true, + solver_transfer_reward_enabled: true, + ..Default::default() + } + } + + #[test] + fn test_disabled_returns_none() { + let config = PoCWConfig::default(); // enabled: false + let events = vec![make_transfer_event(Some("solver-1"))]; + assert!(process_fold(&config, &events).is_none()); + } + + #[test] + fn test_empty_fold() { + let config = enabled_config(); + let result = process_fold(&config, &[]).unwrap(); + assert_eq!(result.event_count, 0); + assert_eq!(result.total_flux_burned, 0); + assert_eq!(result.total_power_consumed, 0); + assert_eq!(result.total_solver_rewards, 0); + assert!(result.solver_rewards.is_empty()); + } + + #[test] + fn test_single_solver() { + let config = enabled_config(); + let events = vec![ + make_transfer_event(Some("solver-1")), + make_transfer_event(Some("solver-1")), + make_transfer_event(Some("solver-1")), + ]; + + let result = process_fold(&config, &events).unwrap(); + assert_eq!(result.event_count, 3); + assert_eq!(result.total_flux_burned, 21_000 * 3); + assert_eq!(result.total_power_consumed, 1 * 3); + assert_eq!(result.total_solver_rewards, 1 * 3); + assert_eq!(result.solver_rewards.len(), 1); + assert_eq!(result.solver_rewards[0].solver_id, "solver-1"); + assert_eq!(result.solver_rewards[0].transfer_count, 3); + assert_eq!(result.solver_rewards[0].flux_reward, 3); + } + + #[test] + fn test_multi_solver() { + let config = enabled_config(); + let events = vec![ + make_transfer_event(Some("solver-1")), + make_transfer_event(Some("solver-2")), + make_transfer_event(Some("solver-1")), + make_transfer_event(Some("solver-2")), + make_transfer_event(Some("solver-2")), + ]; + + let result = process_fold(&config, &events).unwrap(); + assert_eq!(result.event_count, 5); + assert_eq!(result.total_flux_burned, 21_000 * 5); + assert_eq!(result.total_solver_rewards, 5); // 2 + 3 + + // Sorted by solver_id + assert_eq!(result.solver_rewards[0].solver_id, "solver-1"); + assert_eq!(result.solver_rewards[0].transfer_count, 2); + assert_eq!(result.solver_rewards[0].flux_reward, 2); + assert_eq!(result.solver_rewards[1].solver_id, "solver-2"); + assert_eq!(result.solver_rewards[1].transfer_count, 3); + assert_eq!(result.solver_rewards[1].flux_reward, 3); + } + + #[test] + fn test_reward_disabled() { + let config = PoCWConfig { + enabled: true, + solver_transfer_reward_enabled: false, + ..Default::default() + }; + let events = vec![make_transfer_event(Some("solver-1"))]; + + let result = process_fold(&config, &events).unwrap(); + assert_eq!(result.total_flux_burned, 21_000); + assert_eq!(result.total_solver_rewards, 0); + assert!(result.solver_rewards.is_empty()); + } + + #[test] + fn test_non_transfer_events_ignored() { + let config = enabled_config(); + let events = vec![ + make_transfer_event(Some("solver-1")), + make_genesis_event(), // not a transfer + ]; + + let result = process_fold(&config, &events).unwrap(); + assert_eq!(result.event_count, 2); // total events in fold + assert_eq!(result.total_flux_burned, 21_000); // only 1 transfer burned + assert_eq!(result.total_solver_rewards, 1); + } + + #[test] + fn test_transfer_without_executed_by() { + let config = enabled_config(); + let events = vec![ + make_transfer_event(Some("solver-1")), + make_transfer_event(None), // no solver recorded + ]; + + let result = process_fold(&config, &events).unwrap(); + // Both transfers still burn and drain + assert_eq!(result.total_flux_burned, 21_000 * 2); + assert_eq!(result.total_power_consumed, 2); + // Only the one with executed_by gets a reward + assert_eq!(result.total_solver_rewards, 1); + assert_eq!(result.solver_rewards.len(), 1); + } +} diff --git a/types/src/lib.rs b/types/src/lib.rs index 00995ea..5a29d94 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -81,7 +81,7 @@ pub use merkle::{ pub use account_view::AccountView; // Task types for Validator → Solver communication -pub use pocw::PoCWConfig; +pub use pocw::{PoCWConfig, SolverReward, FoldEconomics}; pub use task::{ SolverTask, ResolvedInputs, OperationType, ResolvedObject, diff --git a/types/src/pocw.rs b/types/src/pocw.rs index 2403e18..205c889 100644 --- a/types/src/pocw.rs +++ b/types/src/pocw.rs @@ -35,6 +35,31 @@ impl Default for PoCWConfig { } } +/// Solver reward record for a single solver within a fold +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SolverReward { + pub solver_id: String, + /// Number of FluxTransfer events this solver processed in the fold + pub transfer_count: u64, + /// Flux reward for this solver + pub flux_reward: u64, +} + +/// Fold-level economic summary +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FoldEconomics { + /// Number of events in this fold + pub event_count: usize, + /// Total Flux burned across all events in this fold + pub total_flux_burned: u64, + /// Total power drained across all events in this fold + pub total_power_consumed: u64, + /// Total Flux minted as solver rewards in this fold + pub total_solver_rewards: u64, + /// Per-solver reward breakdown + pub solver_rewards: Vec, +} + #[cfg(test)] mod tests { use super::*; From bd6cd0d90f0bb52cac6e9dde976c54ac934597c9 Mon Sep 17 00:00:00 2001 From: ABresting Date: Sat, 14 Feb 2026 00:54:24 +0100 Subject: [PATCH 05/12] pocw: add fold observer and wire into consensus pipeline (#8) FoldObserver hooks into ConsensusManager after fold commit to compute economic summaries. Sets executed_by on events in tee_executor. --- consensus/src/folder.rs | 19 ++- consensus/src/pocw/fold_observer.rs | 114 ++++++++++++++++++ consensus/src/pocw/mod.rs | 1 + setu-validator/src/network/tee_executor.rs | 2 + .../tests/persistence_recovery_test.rs | 1 + 5 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 consensus/src/pocw/fold_observer.rs diff --git a/consensus/src/folder.rs b/consensus/src/folder.rs index 9f0b51f..b04363e 100644 --- a/consensus/src/folder.rs +++ b/consensus/src/folder.rs @@ -3,6 +3,7 @@ use setu_types::{ }; use crate::anchor_builder::{AnchorBuilder, AnchorBuildResult, AnchorBuildError, PendingAnchorBuild}; use crate::dag::Dag; +use crate::pocw::fold_observer::FoldObserver; use crate::vlc::VLC; use setu_storage::subnet_state::GlobalStateManager; use std::collections::HashMap; @@ -117,11 +118,16 @@ pub struct ConsensusManager { local_validator_id: String, /// Last build result for diagnostics last_build_result: Option, + /// PoCW fold observer (active when PoCWConfig::enabled) + fold_observer: Option, } impl ConsensusManager { /// Create a new ConsensusManager with AnchorBuilder pub fn new(config: ConsensusConfig, validator_id: String) -> Self { + let fold_observer = config.pocw + .filter(|p| p.enabled) + .map(FoldObserver::new); Self { config: config.clone(), anchor_builder: AnchorBuilder::new(config.clone()), @@ -132,15 +138,19 @@ impl ConsensusManager { persisted_anchor_ids: std::collections::HashSet::new(), local_validator_id: validator_id, last_build_result: None, + fold_observer, } } /// Create with an existing GlobalStateManager (for state persistence) pub fn with_state_manager( - config: ConsensusConfig, + config: ConsensusConfig, validator_id: String, state_manager: GlobalStateManager, ) -> Self { + let fold_observer = config.pocw + .filter(|p| p.enabled) + .map(FoldObserver::new); Self { config: config.clone(), anchor_builder: AnchorBuilder::with_state_manager(config.clone(), state_manager), @@ -151,6 +161,7 @@ impl ConsensusManager { persisted_anchor_ids: std::collections::HashSet::new(), local_validator_id: validator_id, last_build_result: None, + fold_observer, } } @@ -304,6 +315,12 @@ impl ConsensusManager { // Leader path: commit the pending build match self.anchor_builder.commit_build(pending_build.clone()) { Ok(state_summary) => { + // PoCW: observe the committed fold + if let Some(ref mut observer) = self.fold_observer { + let fold_events = pending_build.all_events(); + observer.on_fold_committed(&fold_events); + } + // Store result for diagnostics self.last_build_result = Some(AnchorBuildResult { anchor: cf.anchor.clone(), diff --git a/consensus/src/pocw/fold_observer.rs b/consensus/src/pocw/fold_observer.rs new file mode 100644 index 0000000..df77a7f --- /dev/null +++ b/consensus/src/pocw/fold_observer.rs @@ -0,0 +1,114 @@ +//! FoldObserver watches for fold commits and triggers economic processing. +//! +//! Sits as a parallel path alongside existing consensus — does not modify +//! anchor building, voting, or state application. + +use setu_types::pocw::{PoCWConfig, FoldEconomics}; +use setu_types::Event; +use tracing::info; + +use super::processor; + +/// Observes fold commits and produces economic summaries. +pub struct FoldObserver { + config: PoCWConfig, + /// History of fold economics for diagnostics + history: Vec, +} + +impl FoldObserver { + pub fn new(config: PoCWConfig) -> Self { + Self { + config, + history: Vec::new(), + } + } + + /// Called after a fold is committed. Processes economics if enabled. + pub fn on_fold_committed(&mut self, events: &[Event]) -> Option { + let result = processor::process_fold(&self.config, events)?; + + info!( + event_count = result.event_count, + total_flux_burned = result.total_flux_burned, + total_power_consumed = result.total_power_consumed, + total_solver_rewards = result.total_solver_rewards, + solver_count = result.solver_rewards.len(), + "Fold economics processed" + ); + + self.history.push(result.clone()); + Some(result) + } + + pub fn history(&self) -> &[FoldEconomics] { + &self.history + } + + pub fn config(&self) -> &PoCWConfig { + &self.config + } +} + +#[cfg(test)] +mod tests { + use super::*; + use setu_types::event::{EventType, VLCSnapshot}; + + fn make_transfer(solver: &str) -> Event { + let mut event = Event::new( + EventType::Transfer, + vec![], + VLCSnapshot::new(), + "validator-1".to_string(), + ); + event.executed_by = Some(solver.to_string()); + event + } + + #[test] + fn test_disabled_produces_none() { + let mut observer = FoldObserver::new(PoCWConfig::default()); + let events = vec![make_transfer("solver-1")]; + assert!(observer.on_fold_committed(&events).is_none()); + assert!(observer.history().is_empty()); + } + + #[test] + fn test_enabled_produces_economics() { + let config = PoCWConfig { + enabled: true, + solver_transfer_reward_enabled: true, + ..Default::default() + }; + let mut observer = FoldObserver::new(config); + let events = vec![ + make_transfer("solver-1"), + make_transfer("solver-2"), + make_transfer("solver-1"), + ]; + + let result = observer.on_fold_committed(&events).unwrap(); + assert_eq!(result.event_count, 3); + assert_eq!(result.total_flux_burned, 21_000 * 3); + assert_eq!(result.total_solver_rewards, 3); + assert_eq!(result.solver_rewards.len(), 2); + assert_eq!(observer.history().len(), 1); + } + + #[test] + fn test_history_accumulates() { + let config = PoCWConfig { + enabled: true, + ..Default::default() + }; + let mut observer = FoldObserver::new(config); + + observer.on_fold_committed(&[make_transfer("s1")]); + observer.on_fold_committed(&[make_transfer("s2"), make_transfer("s2")]); + + assert_eq!(observer.history().len(), 2); + assert_eq!(observer.history()[0].event_count, 1); + assert_eq!(observer.history()[1].event_count, 2); + } +} diff --git a/consensus/src/pocw/mod.rs b/consensus/src/pocw/mod.rs index c1bea3e..d308153 100644 --- a/consensus/src/pocw/mod.rs +++ b/consensus/src/pocw/mod.rs @@ -5,3 +5,4 @@ pub mod flux_burn; pub mod power; pub mod processor; +pub mod fold_observer; diff --git a/setu-validator/src/network/tee_executor.rs b/setu-validator/src/network/tee_executor.rs index 951afb3..d2024ef 100644 --- a/setu-validator/src/network/tee_executor.rs +++ b/setu-validator/src/network/tee_executor.rs @@ -214,6 +214,7 @@ impl TeeExecutor { }; event.set_execution_result(execution_result); + event.executed_by = Some(solver_id.clone()); event.status = setu_types::event::EventStatus::Executed; // 6. Submit to consensus (if enabled) @@ -484,6 +485,7 @@ pub async fn send_solver_task_sync( }; event.set_execution_result(execution_result); + event.executed_by = Some(solver_id.to_string()); event.status = setu_types::event::EventStatus::Executed; Ok((event, exec_response)) diff --git a/setu-validator/tests/persistence_recovery_test.rs b/setu-validator/tests/persistence_recovery_test.rs index 0420b8c..3e942b8 100644 --- a/setu-validator/tests/persistence_recovery_test.rs +++ b/setu-validator/tests/persistence_recovery_test.rs @@ -55,6 +55,7 @@ fn create_test_event(id: &str, creator: &str, parent_ids: Vec) -> Event execution_result: None, status: EventStatus::Pending, vlc_snapshot: VLCSnapshot::new(), + executed_by: None, } } From 1aa7a056c34db436f489a63e9a38dd9e6021e82a Mon Sep 17 00:00:00 2001 From: ABresting Date: Sat, 14 Feb 2026 01:02:54 +0100 Subject: [PATCH 06/12] pocw: wire burn fee into transfer execution pipeline (#8) Transfer.burn_fee flows from validator through enclave to runtime. Sender pays amount + burn_fee; recipient gets only amount. Burned flux is destroyed (supply reduction). Zero burn_fee preserves existing behavior. --- crates/setu-enclave/src/mock/mod.rs | 6 +- crates/setu-runtime/src/executor.rs | 209 ++++++++++++++++++++++--- crates/setu-runtime/src/transaction.rs | 19 ++- types/src/transfer.rs | 14 +- 4 files changed, 217 insertions(+), 31 deletions(-) diff --git a/crates/setu-enclave/src/mock/mod.rs b/crates/setu-enclave/src/mock/mod.rs index cf6a863..4db0085 100644 --- a/crates/setu-enclave/src/mock/mod.rs +++ b/crates/setu-enclave/src/mock/mod.rs @@ -409,11 +409,12 @@ impl MockEnclave { // Use the coin_id from resolved_inputs let output = { let mut runtime = self.runtime.write().await; - runtime.execute_transfer_with_coin( + runtime.execute_transfer_with_coin_and_burn( resolved_coin.object_id.clone(), &transfer.from, &transfer.to, Some(transfer.amount as u64), + transfer.burn_fee, &ctx, ).map_err(|e| format!("Runtime error: {}", e))? }; @@ -473,11 +474,12 @@ impl MockEnclave { ); // Use the LOCAL runtime (not self.runtime!) - let output = local_runtime.execute_transfer_with_coin( + let output = local_runtime.execute_transfer_with_coin_and_burn( resolved_coin.object_id.clone(), &transfer.from, &transfer.to, Some(transfer.amount as u64), + transfer.burn_fee, &ctx, ).map_err(|e| format!("Runtime error: {}", e))?; diff --git a/crates/setu-runtime/src/executor.rs b/crates/setu-runtime/src/executor.rs index dc6ed8d..3f50941 100644 --- a/crates/setu-runtime/src/executor.rs +++ b/crates/setu-runtime/src/executor.rs @@ -152,27 +152,35 @@ impl RuntimeExecutor { let mut created_objects = Vec::new(); let deleted_objects = Vec::new(); - // 3. 执行转账逻辑 + let burn_fee = transfer_tx.burn_fee; + + // 3. Execute transfer logic match transfer_tx.amount { - // 完整转账:直接转移对象所有权 + // Full transfer: transfer ownership (burn fee deducted from coin value) None => { + // Deduct burn fee before transferring ownership + if burn_fee > 0 { + coin.data.balance.withdraw(burn_fee) + .map_err(|e| RuntimeError::InvalidTransaction( + format!("Insufficient balance for burn fee: {}", e) + ))?; + } + debug!( coin_id = %coin_id, from = %tx.sender, to = %recipient, amount = coin.data.balance.value(), + burn_fee = burn_fee, "Full transfer" ); - - // 更改所有者 + coin.metadata.owner = Some(recipient.clone()); coin.metadata.version += 1; - + let new_state = serde_json::to_vec(&coin)?; - - // 保存更新后的对象 self.state.set_object(coin_id, coin)?; - + state_changes.push(StateChange { change_type: StateChangeType::Update, object_id: coin_id, @@ -180,45 +188,52 @@ impl RuntimeExecutor { new_state: Some(new_state), }); } - - // 部分转账:需要分割 Coin + + // Partial transfer: split coin (burn fee deducted from sender, not sent to recipient) Some(amount) => { + let total_deduction = amount.checked_add(burn_fee) + .ok_or_else(|| RuntimeError::InvalidTransaction( + "Amount + burn_fee overflow".to_string() + ))?; + debug!( coin_id = %coin_id, from = %tx.sender, to = %recipient, amount = amount, - remaining = coin.data.balance.value() - amount, + burn_fee = burn_fee, + total_deduction = total_deduction, "Partial transfer (split)" ); - - // 从原 Coin 中提取金额 - let transferred_balance = coin.data.balance.withdraw(amount) + + // Withdraw amount + burn_fee from sender + coin.data.balance.withdraw(total_deduction) .map_err(|e| RuntimeError::InvalidTransaction(e))?; - - // 更新原 Coin + + // Update sender's coin (reduced by amount + burn_fee) coin.metadata.version += 1; let new_state = serde_json::to_vec(&coin)?; self.state.set_object(coin_id, coin.clone())?; - + state_changes.push(StateChange { change_type: StateChangeType::Update, object_id: coin_id, old_state: Some(old_state), new_state: Some(new_state), }); - - // 创建新 Coin 给接收者 + + // Create new coin for recipient with only the transfer amount + // (burn_fee is destroyed — no coin created for it) let new_coin = create_typed_coin( recipient.clone(), - transferred_balance.value(), + amount, coin.data.coin_type.as_str(), ); let new_coin_id = *new_coin.id(); let new_coin_state = serde_json::to_vec(&new_coin)?; - + self.state.set_object(new_coin_id, new_coin)?; - + created_objects.push(new_coin_id); state_changes.push(StateChange { change_type: StateChangeType::Create, @@ -328,26 +343,44 @@ impl RuntimeExecutor { recipient: &str, amount: Option, ctx: &ExecutionContext, + ) -> RuntimeResult { + self.execute_transfer_with_coin_and_burn(coin_id, sender, recipient, amount, 0, ctx) + } + + /// Execute a transfer with burn fee (solver-tee3 architecture) + /// + /// The burn_fee is deducted from the sender's coin and destroyed (not + /// transferred to any recipient). This reduces total supply. + pub fn execute_transfer_with_coin_and_burn( + &mut self, + coin_id: ObjectId, + sender: &str, + recipient: &str, + amount: Option, + burn_fee: u64, + ctx: &ExecutionContext, ) -> RuntimeResult { let sender_addr = Address::from(sender); let recipient_addr = Address::from(recipient); - + info!( coin_id = %coin_id, from = %sender, to = %recipient, amount = ?amount, + burn_fee = burn_fee, "Executing transfer with specified coin_id" ); - + // Create and execute the transfer transaction - let tx = Transaction::new_transfer( + let tx = Transaction::new_transfer_with_burn( sender_addr, coin_id, recipient_addr, amount, + burn_fee, ); - + self.execute_transaction(&tx, ctx) } @@ -535,4 +568,128 @@ mod tests { assert_eq!(new_coin.data.balance.value(), 300); assert_eq!(new_coin.metadata.owner.unwrap(), recipient); } + + #[test] + fn test_partial_transfer_with_burn_fee() { + let mut store = InMemoryStateStore::new(); + let sender = Address::from("alice"); + let recipient = Address::from("bob"); + + let coin = setu_types::create_coin(sender.clone(), 100_000); + let coin_id = *coin.id(); + store.set_object(coin_id, coin).unwrap(); + + let mut executor = RuntimeExecutor::new(store); + let ctx = ExecutionContext { + executor_id: "solver1".to_string(), + timestamp: 1000, + in_tee: false, + }; + + // Transfer 5000 with 21000 burn fee → sender loses 26000 total + let tx = Transaction::new_transfer_with_burn( + sender.clone(), coin_id, recipient.clone(), Some(5000), 21_000, + ); + + let output = executor.execute_transaction(&tx, &ctx).unwrap(); + assert!(output.success); + + // Sender: 100_000 - 5000 - 21000 = 74_000 + let original = executor.state().get_object(&coin_id).unwrap().unwrap(); + assert_eq!(original.data.balance.value(), 74_000); + + // Recipient gets only the transfer amount, not the burn + let new_coin_id = output.created_objects[0]; + let new_coin = executor.state().get_object(&new_coin_id).unwrap().unwrap(); + assert_eq!(new_coin.data.balance.value(), 5000); + } + + #[test] + fn test_full_transfer_with_burn_fee() { + let mut store = InMemoryStateStore::new(); + let sender = Address::from("alice"); + let recipient = Address::from("bob"); + + let coin = setu_types::create_coin(sender.clone(), 50_000); + let coin_id = *coin.id(); + store.set_object(coin_id, coin).unwrap(); + + let mut executor = RuntimeExecutor::new(store); + let ctx = ExecutionContext { + executor_id: "solver1".to_string(), + timestamp: 1000, + in_tee: false, + }; + + // Full transfer with 21000 burn → coin arrives with 50_000 - 21_000 = 29_000 + let tx = Transaction::new_transfer_with_burn( + sender.clone(), coin_id, recipient.clone(), None, 21_000, + ); + + let output = executor.execute_transaction(&tx, &ctx).unwrap(); + assert!(output.success); + + let coin = executor.state().get_object(&coin_id).unwrap().unwrap(); + assert_eq!(coin.data.balance.value(), 29_000); + assert_eq!(coin.metadata.owner.unwrap(), recipient); + } + + #[test] + fn test_burn_fee_insufficient_balance() { + let mut store = InMemoryStateStore::new(); + let sender = Address::from("alice"); + let recipient = Address::from("bob"); + + let coin = setu_types::create_coin(sender.clone(), 1000); + let coin_id = *coin.id(); + store.set_object(coin_id, coin).unwrap(); + + let mut executor = RuntimeExecutor::new(store); + let ctx = ExecutionContext { + executor_id: "solver1".to_string(), + timestamp: 1000, + in_tee: false, + }; + + // Transfer 500 with 21000 burn fee → needs 21500, has 1000 + let tx = Transaction::new_transfer_with_burn( + sender.clone(), coin_id, recipient.clone(), Some(500), 21_000, + ); + + let result = executor.execute_transaction(&tx, &ctx); + assert!(result.is_err()); + } + + #[test] + fn test_zero_burn_fee_unchanged_behavior() { + let mut store = InMemoryStateStore::new(); + let sender = Address::from("alice"); + let recipient = Address::from("bob"); + + let coin = setu_types::create_coin(sender.clone(), 1000); + let coin_id = *coin.id(); + store.set_object(coin_id, coin).unwrap(); + + let mut executor = RuntimeExecutor::new(store); + let ctx = ExecutionContext { + executor_id: "solver1".to_string(), + timestamp: 1000, + in_tee: false, + }; + + // Zero burn_fee = identical to old behavior + let tx = Transaction::new_transfer_with_burn( + sender.clone(), coin_id, recipient.clone(), Some(300), 0, + ); + + let output = executor.execute_transaction(&tx, &ctx).unwrap(); + assert!(output.success); + + let original = executor.state().get_object(&coin_id).unwrap().unwrap(); + assert_eq!(original.data.balance.value(), 700); + + let new_coin_id = output.created_objects[0]; + let new_coin = executor.state().get_object(&new_coin_id).unwrap().unwrap(); + assert_eq!(new_coin.data.balance.value(), 300); + } } diff --git a/crates/setu-runtime/src/transaction.rs b/crates/setu-runtime/src/transaction.rs index 7180b75..0917994 100644 --- a/crates/setu-runtime/src/transaction.rs +++ b/crates/setu-runtime/src/transaction.rs @@ -36,6 +36,9 @@ pub struct TransferTx { pub recipient: Address, /// Transfer amount (if partial transfer) pub amount: Option, + /// Burn fee deducted from sender (destroyed, not transferred) + #[serde(default)] + pub burn_fee: u64, } /// Query transaction (read-only) @@ -64,14 +67,25 @@ impl Transaction { coin_id: ObjectId, recipient: Address, amount: Option, + ) -> Self { + Self::new_transfer_with_burn(sender, coin_id, recipient, amount, 0) + } + + /// Create a transfer transaction with burn fee + pub fn new_transfer_with_burn( + sender: Address, + coin_id: ObjectId, + recipient: Address, + amount: Option, + burn_fee: u64, ) -> Self { let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis() as u64; - + let id = format!("tx_{:x}", timestamp); - + Self { id, sender, @@ -79,6 +93,7 @@ impl Transaction { coin_id, recipient, amount, + burn_fee, }), input_objects: vec![coin_id], timestamp, diff --git a/types/src/transfer.rs b/types/src/transfer.rs index babc10c..29dc67a 100644 --- a/types/src/transfer.rs +++ b/types/src/transfer.rs @@ -78,6 +78,10 @@ pub struct Transfer { /// Power/work score used for tie-breaks in ordering pub power: u64, + + /// Burn fee deducted from sender on execution (set by validator from PoCWConfig) + #[serde(default)] + pub burn_fee: u64, // ========== Routing Hints ========== @@ -108,13 +112,20 @@ impl Transfer { transfer_type: TransferType::FluxTransfer, resources: vec![], power: 0, + burn_fee: 0, preferred_solver: None, shard_id: None, subnet_id: None, assigned_vlc: None, } } - + + /// Set burn fee (typically called by validator from PoCWConfig) + pub fn with_burn_fee(mut self, burn_fee: u64) -> Self { + self.burn_fee = burn_fee; + self + } + /// Set transfer type pub fn with_type(mut self, transfer_type: TransferType) -> Self { self.transfer_type = transfer_type; @@ -188,6 +199,7 @@ impl Default for Transfer { transfer_type: TransferType::default(), resources: vec![], power: 0, + burn_fee: 0, preferred_solver: None, shard_id: None, subnet_id: None, From ebb3b42b2c94d2f7ebdbd59f5cdd7ff9548428a7 Mon Sep 17 00:00:00 2001 From: ABresting Date: Sat, 14 Feb 2026 01:10:14 +0100 Subject: [PATCH 07/12] pocw: wire solver reward minting into state at fold time (#8) After fold commit, FoldObserver computes economics and apply_reward_minting writes cumulative rewards to ROOT subnet SMT under "pocw:reward:{solver_id}" keys. Rewards accumulate across folds. Adds get_value() to GlobalStateManager for reading state by key. --- consensus/src/folder.rs | 121 +++++++++++++++++++++++++++++++++-- storage/src/state/manager.rs | 6 ++ 2 files changed, 123 insertions(+), 4 deletions(-) diff --git a/consensus/src/folder.rs b/consensus/src/folder.rs index b04363e..4532881 100644 --- a/consensus/src/folder.rs +++ b/consensus/src/folder.rs @@ -1,5 +1,6 @@ use setu_types::{ - Anchor, ConsensusConfig, ConsensusFrame, EventId, Vote, + Anchor, ConsensusConfig, ConsensusFrame, EventId, SubnetId, Vote, + event::StateChange, }; use crate::anchor_builder::{AnchorBuilder, AnchorBuildResult, AnchorBuildError, PendingAnchorBuild}; use crate::dag::Dag; @@ -315,10 +316,12 @@ impl ConsensusManager { // Leader path: commit the pending build match self.anchor_builder.commit_build(pending_build.clone()) { Ok(state_summary) => { - // PoCW: observe the committed fold + // PoCW: observe the committed fold and mint rewards if let Some(ref mut observer) = self.fold_observer { let fold_events = pending_build.all_events(); - observer.on_fold_committed(&fold_events); + if let Some(economics) = observer.on_fold_committed(&fold_events) { + self.apply_reward_minting(&economics); + } } // Store result for diagnostics @@ -467,7 +470,55 @@ impl ConsensusManager { } // ========================================================================= - // New methods for Merkle tree access + // PoCW Reward Minting + // ========================================================================= + + /// Mint solver rewards by writing StateChanges to the ROOT subnet SMT. + /// + /// For each solver with a non-zero reward, reads the current accumulated + /// balance from state, adds the new reward, and writes back. The key + /// namespace is "pocw:reward:{solver_id}" to avoid conflicts with coin + /// objects. Rewards are persisted across folds. + fn apply_reward_minting(&mut self, economics: &setu_types::pocw::FoldEconomics) { + let state_manager = self.anchor_builder.state_manager_mut(); + + for reward in &economics.solver_rewards { + if reward.flux_reward == 0 { + continue; + } + + let key = format!("pocw:reward:{}", reward.solver_id); + + // Read current accumulated reward + let old_balance = state_manager + .get_value(&SubnetId::ROOT, &key) + .and_then(|bytes| { + if bytes.len() == 8 { + Some(u64::from_le_bytes(bytes.try_into().unwrap())) + } else { + None + } + }) + .unwrap_or(0); + + let new_balance = old_balance.saturating_add(reward.flux_reward); + + let change = StateChange::new( + key, + if old_balance > 0 { + Some(old_balance.to_le_bytes().to_vec()) + } else { + None + }, + Some(new_balance.to_le_bytes().to_vec()), + ); + + state_manager.apply_state_change(SubnetId::ROOT, &change); + } + } + + // ========================================================================= + // Merkle tree access // ========================================================================= /// Get the AnchorBuilder (read-only) @@ -687,4 +738,66 @@ mod tests { let global_root = manager.get_global_root(); assert_ne!(global_root, [0u8; 32]); } + + #[test] + fn test_apply_reward_minting() { + use setu_types::pocw::{PoCWConfig, FoldEconomics, SolverReward}; + + let config = ConsensusConfig { + pocw: Some(PoCWConfig { + enabled: true, + solver_transfer_reward_enabled: true, + ..Default::default() + }), + ..Default::default() + }; + let mut manager = ConsensusManager::new(config, "v1".to_string()); + + // Simulate fold economics with two solvers + let economics = FoldEconomics { + event_count: 5, + total_flux_burned: 105_000, + total_power_consumed: 5, + total_solver_rewards: 5, + solver_rewards: vec![ + SolverReward { solver_id: "solver-a".into(), transfer_count: 3, flux_reward: 3 }, + SolverReward { solver_id: "solver-b".into(), transfer_count: 2, flux_reward: 2 }, + ], + }; + + // Apply minting + manager.apply_reward_minting(&economics); + + // Verify rewards persisted in state + { + let sm = manager.state_manager(); + let val_a = sm.get_value(&SubnetId::ROOT, "pocw:reward:solver-a").unwrap(); + assert_eq!(u64::from_le_bytes(val_a.try_into().unwrap()), 3); + + let val_b = sm.get_value(&SubnetId::ROOT, "pocw:reward:solver-b").unwrap(); + assert_eq!(u64::from_le_bytes(val_b.try_into().unwrap()), 2); + } + + // Apply a second fold — rewards accumulate + let economics2 = FoldEconomics { + event_count: 2, + total_flux_burned: 42_000, + total_power_consumed: 2, + total_solver_rewards: 2, + solver_rewards: vec![ + SolverReward { solver_id: "solver-a".into(), transfer_count: 2, flux_reward: 2 }, + ], + }; + manager.apply_reward_minting(&economics2); + + { + let sm = manager.state_manager(); + let val_a = sm.get_value(&SubnetId::ROOT, "pocw:reward:solver-a").unwrap(); + assert_eq!(u64::from_le_bytes(val_a.try_into().unwrap()), 5); // 3 + 2 + + // solver-b unchanged + let val_b = sm.get_value(&SubnetId::ROOT, "pocw:reward:solver-b").unwrap(); + assert_eq!(u64::from_le_bytes(val_b.try_into().unwrap()), 2); + } + } } diff --git a/storage/src/state/manager.rs b/storage/src/state/manager.rs index 7d7ed62..a453776 100644 --- a/storage/src/state/manager.rs +++ b/storage/src/state/manager.rs @@ -465,6 +465,12 @@ impl GlobalStateManager { Ok((anchor_roots, summary)) } + /// Read a value from a subnet by string key + pub fn get_value(&self, subnet_id: &SubnetId, key: &str) -> Option> { + let object_id = Self::key_to_object_id(key); + self.get_subnet(subnet_id)?.get(&object_id).cloned() + } + /// Convert a string key to a 32-byte ObjectId using SHA-256 fn key_to_object_id(key: &str) -> HashValue { let mut hasher = Sha256::new(); From afcc18112597b20e767996c66936d19777cbbaab Mon Sep 17 00:00:00 2001 From: ABresting Date: Sat, 14 Feb 2026 01:19:41 +0100 Subject: [PATCH 08/12] pocw: wire burn fee from PoCWConfig into validator transfer pipeline (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thread transfer_burn_fee from PoCWConfig through NetworkServiceConfig to TransferHandler so every FluxTransfer carries the configured burn fee. When PoCW is disabled (default), burn_fee remains 0 — no change in behavior. --- setu-validator/src/main.rs | 7 +++++++ setu-validator/src/network/service.rs | 1 + setu-validator/src/network/transfer_handler.rs | 4 +++- setu-validator/src/network/types.rs | 3 +++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/setu-validator/src/main.rs b/setu-validator/src/main.rs index 8a5f4b2..3231da3 100644 --- a/setu-validator/src/main.rs +++ b/setu-validator/src/main.rs @@ -168,6 +168,12 @@ async fn main() -> anyhow::Result<()> { consensus_config.is_leader = true; // Single node mode: always leader consensus_config.consensus.validator_count = 1; // Single node mode: only 1 validator + // Extract burn fee before consensus_config is consumed + let transfer_burn_fee = consensus_config.consensus.pocw + .filter(|p| p.enabled) + .map(|p| p.transfer_fixed_fee) + .unwrap_or(0); + // Create ConsensusValidator with appropriate storage backend let consensus_validator = if let Some(ref db_path) = config.db_path { // RocksDB persistence mode - open database and create all backends @@ -215,6 +221,7 @@ async fn main() -> anyhow::Result<()> { let network_config = NetworkServiceConfig { http_listen_addr: config.http_addr, p2p_listen_addr: config.p2p_addr, + transfer_burn_fee, }; // Create network service with consensus enabled diff --git a/setu-validator/src/network/service.rs b/setu-validator/src/network/service.rs index b4e632c..507b415 100644 --- a/setu-validator/src/network/service.rs +++ b/setu-validator/src/network/service.rs @@ -399,6 +399,7 @@ impl ValidatorNetworkService { vlc_time, request, &self.tee_executor, + self.config.transfer_burn_fee, ) .await } diff --git a/setu-validator/src/network/transfer_handler.rs b/setu-validator/src/network/transfer_handler.rs index a345f3b..fa1a746 100644 --- a/setu-validator/src/network/transfer_handler.rs +++ b/setu-validator/src/network/transfer_handler.rs @@ -41,6 +41,7 @@ impl TransferHandler { vlc_time: u64, request: SubmitTransferRequest, tee_executor: &TeeExecutor, + burn_fee: u64, ) -> SubmitTransferResponse { let now = current_timestamp_secs(); let transfer_id = format!( @@ -112,7 +113,8 @@ impl TransferHandler { .with_preferred_solver_opt(request.preferred_solver.clone()) .with_shard_id(request.shard_id.clone()) .with_subnet_id(request.subnet_id.clone()) - .with_assigned_vlc(assigned_vlc); + .with_assigned_vlc(assigned_vlc) + .with_burn_fee(burn_fee); // Step 4a: Prepare SolverTask let subnet_id = match &transfer.subnet_id { diff --git a/setu-validator/src/network/types.rs b/setu-validator/src/network/types.rs index 07a2ee2..8fab5c9 100644 --- a/setu-validator/src/network/types.rs +++ b/setu-validator/src/network/types.rs @@ -23,6 +23,8 @@ pub struct NetworkServiceConfig { pub http_listen_addr: SocketAddr, /// Listen address for Anemo P2P pub p2p_listen_addr: SocketAddr, + /// Burn fee applied to each FluxTransfer (from PoCWConfig, 0 if disabled) + pub transfer_burn_fee: u64, } impl Default for NetworkServiceConfig { @@ -30,6 +32,7 @@ impl Default for NetworkServiceConfig { Self { http_listen_addr: "127.0.0.1:8080".parse().unwrap(), p2p_listen_addr: "127.0.0.1:9000".parse().unwrap(), + transfer_burn_fee: 0, } } } From 143b40c14b021a7c6d19e93f664cb8c6e4a09760 Mon Sep 17 00:00:00 2001 From: ABresting Date: Sat, 14 Feb 2026 16:13:54 +0100 Subject: [PATCH 09/12] pocw: rename burn_fee to fee, transfer_fixed_fee to transfer_fee (#8) --- consensus/src/pocw/flux_burn.rs | 4 +- crates/setu-enclave/src/mock/mod.rs | 8 +-- crates/setu-runtime/src/executor.rs | 70 +++++++++---------- crates/setu-runtime/src/transaction.rs | 14 ++-- setu-validator/src/main.rs | 8 +-- setu-validator/src/network/service.rs | 2 +- .../src/network/transfer_handler.rs | 4 +- setu-validator/src/network/types.rs | 7 +- types/src/pocw.rs | 14 ++-- types/src/transfer.rs | 15 ++-- 10 files changed, 74 insertions(+), 72 deletions(-) diff --git a/consensus/src/pocw/flux_burn.rs b/consensus/src/pocw/flux_burn.rs index 498929b..4177177 100644 --- a/consensus/src/pocw/flux_burn.rs +++ b/consensus/src/pocw/flux_burn.rs @@ -7,7 +7,7 @@ use setu_types::pocw::PoCWConfig; /// Calculate the Flux burn for a FluxTransfer transaction. pub fn calculate_transfer_burn(config: &PoCWConfig) -> u64 { if config.enabled { - config.transfer_fixed_fee + config.transfer_fee } else { 0 } @@ -36,7 +36,7 @@ mod tests { fn test_custom_fee() { let config = PoCWConfig { enabled: true, - transfer_fixed_fee: 50_000, + transfer_fee: 50_000, ..Default::default() }; assert_eq!(calculate_transfer_burn(&config), 50_000); diff --git a/crates/setu-enclave/src/mock/mod.rs b/crates/setu-enclave/src/mock/mod.rs index 4db0085..c8f5bc3 100644 --- a/crates/setu-enclave/src/mock/mod.rs +++ b/crates/setu-enclave/src/mock/mod.rs @@ -409,12 +409,12 @@ impl MockEnclave { // Use the coin_id from resolved_inputs let output = { let mut runtime = self.runtime.write().await; - runtime.execute_transfer_with_coin_and_burn( + runtime.execute_transfer_with_coin_and_fee( resolved_coin.object_id.clone(), &transfer.from, &transfer.to, Some(transfer.amount as u64), - transfer.burn_fee, + transfer.fee, &ctx, ).map_err(|e| format!("Runtime error: {}", e))? }; @@ -474,12 +474,12 @@ impl MockEnclave { ); // Use the LOCAL runtime (not self.runtime!) - let output = local_runtime.execute_transfer_with_coin_and_burn( + let output = local_runtime.execute_transfer_with_coin_and_fee( resolved_coin.object_id.clone(), &transfer.from, &transfer.to, Some(transfer.amount as u64), - transfer.burn_fee, + transfer.fee, &ctx, ).map_err(|e| format!("Runtime error: {}", e))?; diff --git a/crates/setu-runtime/src/executor.rs b/crates/setu-runtime/src/executor.rs index 3f50941..a884a7d 100644 --- a/crates/setu-runtime/src/executor.rs +++ b/crates/setu-runtime/src/executor.rs @@ -152,17 +152,17 @@ impl RuntimeExecutor { let mut created_objects = Vec::new(); let deleted_objects = Vec::new(); - let burn_fee = transfer_tx.burn_fee; + let fee = transfer_tx.fee; // 3. Execute transfer logic match transfer_tx.amount { - // Full transfer: transfer ownership (burn fee deducted from coin value) + // Full transfer: transfer ownership (fee deducted from coin value, then burned) None => { - // Deduct burn fee before transferring ownership - if burn_fee > 0 { - coin.data.balance.withdraw(burn_fee) + // Deduct fee before transferring ownership + if fee > 0 { + coin.data.balance.withdraw(fee) .map_err(|e| RuntimeError::InvalidTransaction( - format!("Insufficient balance for burn fee: {}", e) + format!("Insufficient balance for fee: {}", e) ))?; } @@ -171,7 +171,7 @@ impl RuntimeExecutor { from = %tx.sender, to = %recipient, amount = coin.data.balance.value(), - burn_fee = burn_fee, + fee = fee, "Full transfer" ); @@ -189,11 +189,11 @@ impl RuntimeExecutor { }); } - // Partial transfer: split coin (burn fee deducted from sender, not sent to recipient) + // Partial transfer: split coin (fee deducted from sender and burned, not sent to recipient) Some(amount) => { - let total_deduction = amount.checked_add(burn_fee) + let total_deduction = amount.checked_add(fee) .ok_or_else(|| RuntimeError::InvalidTransaction( - "Amount + burn_fee overflow".to_string() + "Amount + fee overflow".to_string() ))?; debug!( @@ -201,16 +201,16 @@ impl RuntimeExecutor { from = %tx.sender, to = %recipient, amount = amount, - burn_fee = burn_fee, + fee = fee, total_deduction = total_deduction, "Partial transfer (split)" ); - // Withdraw amount + burn_fee from sender + // Withdraw amount + fee from sender coin.data.balance.withdraw(total_deduction) .map_err(|e| RuntimeError::InvalidTransaction(e))?; - // Update sender's coin (reduced by amount + burn_fee) + // Update sender's coin (reduced by amount + fee) coin.metadata.version += 1; let new_state = serde_json::to_vec(&coin)?; self.state.set_object(coin_id, coin.clone())?; @@ -223,7 +223,7 @@ impl RuntimeExecutor { }); // Create new coin for recipient with only the transfer amount - // (burn_fee is destroyed — no coin created for it) + // (fee is burned/destroyed — no coin created for it) let new_coin = create_typed_coin( recipient.clone(), amount, @@ -344,20 +344,20 @@ impl RuntimeExecutor { amount: Option, ctx: &ExecutionContext, ) -> RuntimeResult { - self.execute_transfer_with_coin_and_burn(coin_id, sender, recipient, amount, 0, ctx) + self.execute_transfer_with_coin_and_fee(coin_id, sender, recipient, amount, 0, ctx) } - /// Execute a transfer with burn fee (solver-tee3 architecture) + /// Execute a transfer with fee (solver-tee3 architecture). /// - /// The burn_fee is deducted from the sender's coin and destroyed (not - /// transferred to any recipient). This reduces total supply. - pub fn execute_transfer_with_coin_and_burn( + /// Fee is deducted from the sender and destroyed. + /// Burn mechanism subject to revision. + pub fn execute_transfer_with_coin_and_fee( &mut self, coin_id: ObjectId, sender: &str, recipient: &str, amount: Option, - burn_fee: u64, + fee: u64, ctx: &ExecutionContext, ) -> RuntimeResult { let sender_addr = Address::from(sender); @@ -368,17 +368,17 @@ impl RuntimeExecutor { from = %sender, to = %recipient, amount = ?amount, - burn_fee = burn_fee, + fee = fee, "Executing transfer with specified coin_id" ); // Create and execute the transfer transaction - let tx = Transaction::new_transfer_with_burn( + let tx = Transaction::new_transfer_with_fee( sender_addr, coin_id, recipient_addr, amount, - burn_fee, + fee, ); self.execute_transaction(&tx, ctx) @@ -570,7 +570,7 @@ mod tests { } #[test] - fn test_partial_transfer_with_burn_fee() { + fn test_partial_transfer_with_fee() { let mut store = InMemoryStateStore::new(); let sender = Address::from("alice"); let recipient = Address::from("bob"); @@ -586,8 +586,8 @@ mod tests { in_tee: false, }; - // Transfer 5000 with 21000 burn fee → sender loses 26000 total - let tx = Transaction::new_transfer_with_burn( + // Transfer 5000 with 21000 fee → sender loses 26000 total + let tx = Transaction::new_transfer_with_fee( sender.clone(), coin_id, recipient.clone(), Some(5000), 21_000, ); @@ -605,7 +605,7 @@ mod tests { } #[test] - fn test_full_transfer_with_burn_fee() { + fn test_full_transfer_with_fee() { let mut store = InMemoryStateStore::new(); let sender = Address::from("alice"); let recipient = Address::from("bob"); @@ -621,8 +621,8 @@ mod tests { in_tee: false, }; - // Full transfer with 21000 burn → coin arrives with 50_000 - 21_000 = 29_000 - let tx = Transaction::new_transfer_with_burn( + // Full transfer with 21000 fee → coin arrives with 50_000 - 21_000 = 29_000 + let tx = Transaction::new_transfer_with_fee( sender.clone(), coin_id, recipient.clone(), None, 21_000, ); @@ -635,7 +635,7 @@ mod tests { } #[test] - fn test_burn_fee_insufficient_balance() { + fn test_fee_insufficient_balance() { let mut store = InMemoryStateStore::new(); let sender = Address::from("alice"); let recipient = Address::from("bob"); @@ -651,8 +651,8 @@ mod tests { in_tee: false, }; - // Transfer 500 with 21000 burn fee → needs 21500, has 1000 - let tx = Transaction::new_transfer_with_burn( + // Transfer 500 with 21000 fee → needs 21500, has 1000 + let tx = Transaction::new_transfer_with_fee( sender.clone(), coin_id, recipient.clone(), Some(500), 21_000, ); @@ -661,7 +661,7 @@ mod tests { } #[test] - fn test_zero_burn_fee_unchanged_behavior() { + fn test_zero_fee_unchanged_behavior() { let mut store = InMemoryStateStore::new(); let sender = Address::from("alice"); let recipient = Address::from("bob"); @@ -677,8 +677,8 @@ mod tests { in_tee: false, }; - // Zero burn_fee = identical to old behavior - let tx = Transaction::new_transfer_with_burn( + // Zero fee = identical to old behavior + let tx = Transaction::new_transfer_with_fee( sender.clone(), coin_id, recipient.clone(), Some(300), 0, ); diff --git a/crates/setu-runtime/src/transaction.rs b/crates/setu-runtime/src/transaction.rs index 0917994..4639bd5 100644 --- a/crates/setu-runtime/src/transaction.rs +++ b/crates/setu-runtime/src/transaction.rs @@ -36,9 +36,9 @@ pub struct TransferTx { pub recipient: Address, /// Transfer amount (if partial transfer) pub amount: Option, - /// Burn fee deducted from sender (destroyed, not transferred) + /// Transaction fee deducted from sender (burned — destroyed, not transferred) #[serde(default)] - pub burn_fee: u64, + pub fee: u64, } /// Query transaction (read-only) @@ -68,16 +68,16 @@ impl Transaction { recipient: Address, amount: Option, ) -> Self { - Self::new_transfer_with_burn(sender, coin_id, recipient, amount, 0) + Self::new_transfer_with_fee(sender, coin_id, recipient, amount, 0) } - /// Create a transfer transaction with burn fee - pub fn new_transfer_with_burn( + /// Create a transfer transaction with fee (fee is burned — destroyed, not paid to anyone) + pub fn new_transfer_with_fee( sender: Address, coin_id: ObjectId, recipient: Address, amount: Option, - burn_fee: u64, + fee: u64, ) -> Self { let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -93,7 +93,7 @@ impl Transaction { coin_id, recipient, amount, - burn_fee, + fee, }), input_objects: vec![coin_id], timestamp, diff --git a/setu-validator/src/main.rs b/setu-validator/src/main.rs index 3231da3..6474be9 100644 --- a/setu-validator/src/main.rs +++ b/setu-validator/src/main.rs @@ -168,10 +168,10 @@ async fn main() -> anyhow::Result<()> { consensus_config.is_leader = true; // Single node mode: always leader consensus_config.consensus.validator_count = 1; // Single node mode: only 1 validator - // Extract burn fee before consensus_config is consumed - let transfer_burn_fee = consensus_config.consensus.pocw + // Extract transfer fee before consensus_config is consumed + let transfer_fee = consensus_config.consensus.pocw .filter(|p| p.enabled) - .map(|p| p.transfer_fixed_fee) + .map(|p| p.transfer_fee) .unwrap_or(0); // Create ConsensusValidator with appropriate storage backend @@ -221,7 +221,7 @@ async fn main() -> anyhow::Result<()> { let network_config = NetworkServiceConfig { http_listen_addr: config.http_addr, p2p_listen_addr: config.p2p_addr, - transfer_burn_fee, + transfer_fee, }; // Create network service with consensus enabled diff --git a/setu-validator/src/network/service.rs b/setu-validator/src/network/service.rs index 507b415..4800623 100644 --- a/setu-validator/src/network/service.rs +++ b/setu-validator/src/network/service.rs @@ -399,7 +399,7 @@ impl ValidatorNetworkService { vlc_time, request, &self.tee_executor, - self.config.transfer_burn_fee, + self.config.transfer_fee, ) .await } diff --git a/setu-validator/src/network/transfer_handler.rs b/setu-validator/src/network/transfer_handler.rs index fa1a746..058d52a 100644 --- a/setu-validator/src/network/transfer_handler.rs +++ b/setu-validator/src/network/transfer_handler.rs @@ -41,7 +41,7 @@ impl TransferHandler { vlc_time: u64, request: SubmitTransferRequest, tee_executor: &TeeExecutor, - burn_fee: u64, + fee: u64, ) -> SubmitTransferResponse { let now = current_timestamp_secs(); let transfer_id = format!( @@ -114,7 +114,7 @@ impl TransferHandler { .with_shard_id(request.shard_id.clone()) .with_subnet_id(request.subnet_id.clone()) .with_assigned_vlc(assigned_vlc) - .with_burn_fee(burn_fee); + .with_fee(fee); // Step 4a: Prepare SolverTask let subnet_id = match &transfer.subnet_id { diff --git a/setu-validator/src/network/types.rs b/setu-validator/src/network/types.rs index 8fab5c9..2853343 100644 --- a/setu-validator/src/network/types.rs +++ b/setu-validator/src/network/types.rs @@ -23,8 +23,9 @@ pub struct NetworkServiceConfig { pub http_listen_addr: SocketAddr, /// Listen address for Anemo P2P pub p2p_listen_addr: SocketAddr, - /// Burn fee applied to each FluxTransfer (from PoCWConfig, 0 if disabled) - pub transfer_burn_fee: u64, + /// Transaction fee applied to each FluxTransfer (from PoCWConfig, 0 if disabled). + /// This fee is burned (destroyed) — it is not paid to any recipient. + pub transfer_fee: u64, } impl Default for NetworkServiceConfig { @@ -32,7 +33,7 @@ impl Default for NetworkServiceConfig { Self { http_listen_addr: "127.0.0.1:8080".parse().unwrap(), p2p_listen_addr: "127.0.0.1:9000".parse().unwrap(), - transfer_burn_fee: 0, + transfer_fee: 0, } } } diff --git a/types/src/pocw.rs b/types/src/pocw.rs index 205c889..39baece 100644 --- a/types/src/pocw.rs +++ b/types/src/pocw.rs @@ -13,8 +13,8 @@ use serde::{Deserialize, Serialize}; pub struct PoCWConfig { /// Master toggle for PoCW economics pub enabled: bool, - /// Fixed gas fee for FluxTransfer transactions - pub transfer_fixed_fee: u64, + /// Fee per FluxTransfer (burned). Burn mechanism subject to revision. + pub transfer_fee: u64, /// Flat power drain per FluxTransfer pub transfer_power_drain: u64, /// Whether solvers receive a nominal reward for processing FluxTransfer transactions @@ -27,7 +27,7 @@ impl Default for PoCWConfig { fn default() -> Self { Self { enabled: false, - transfer_fixed_fee: 21_000, + transfer_fee: 21_000, transfer_power_drain: 1, solver_transfer_reward_enabled: false, solver_transfer_reward: 1, @@ -68,7 +68,7 @@ mod tests { fn test_defaults() { let config = PoCWConfig::default(); assert!(!config.enabled); - assert_eq!(config.transfer_fixed_fee, 21_000); + assert_eq!(config.transfer_fee, 21_000); assert_eq!(config.transfer_power_drain, 1); assert!(!config.solver_transfer_reward_enabled); assert_eq!(config.solver_transfer_reward, 1); @@ -81,7 +81,7 @@ mod tests { let deserialized: PoCWConfig = serde_json::from_str(&json).unwrap(); assert_eq!(config.enabled, deserialized.enabled); - assert_eq!(config.transfer_fixed_fee, deserialized.transfer_fixed_fee); + assert_eq!(config.transfer_fee, deserialized.transfer_fee); assert_eq!(config.transfer_power_drain, deserialized.transfer_power_drain); assert_eq!(config.solver_transfer_reward_enabled, deserialized.solver_transfer_reward_enabled); assert_eq!(config.solver_transfer_reward, deserialized.solver_transfer_reward); @@ -93,14 +93,14 @@ mod tests { enabled: true, solver_transfer_reward_enabled: true, solver_transfer_reward: 5, - transfer_fixed_fee: 10_000, + transfer_fee: 10_000, ..Default::default() }; assert!(config.enabled); assert!(config.solver_transfer_reward_enabled); assert_eq!(config.solver_transfer_reward, 5); - assert_eq!(config.transfer_fixed_fee, 10_000); + assert_eq!(config.transfer_fee, 10_000); assert_eq!(config.transfer_power_drain, 1); } } diff --git a/types/src/transfer.rs b/types/src/transfer.rs index 29dc67a..0c551e0 100644 --- a/types/src/transfer.rs +++ b/types/src/transfer.rs @@ -79,9 +79,10 @@ pub struct Transfer { /// Power/work score used for tie-breaks in ordering pub power: u64, - /// Burn fee deducted from sender on execution (set by validator from PoCWConfig) + /// Transaction fee deducted from sender and destroyed. + /// Burn mechanism subject to revision. #[serde(default)] - pub burn_fee: u64, + pub fee: u64, // ========== Routing Hints ========== @@ -112,7 +113,7 @@ impl Transfer { transfer_type: TransferType::FluxTransfer, resources: vec![], power: 0, - burn_fee: 0, + fee: 0, preferred_solver: None, shard_id: None, subnet_id: None, @@ -120,9 +121,9 @@ impl Transfer { } } - /// Set burn fee (typically called by validator from PoCWConfig) - pub fn with_burn_fee(mut self, burn_fee: u64) -> Self { - self.burn_fee = burn_fee; + /// Set transaction fee (typically called by validator from PoCWConfig) + pub fn with_fee(mut self, fee: u64) -> Self { + self.fee = fee; self } @@ -199,7 +200,7 @@ impl Default for Transfer { transfer_type: TransferType::default(), resources: vec![], power: 0, - burn_fee: 0, + fee: 0, preferred_solver: None, shard_id: None, subnet_id: None, From 21dcc0dc962c1fd367ae06712db5fd4f2b1d8365 Mon Sep 17 00:00:00 2001 From: ABresting Date: Sat, 14 Feb 2026 17:10:49 +0100 Subject: [PATCH 10/12] add dotenv support with auto-load and --env-file flag (#13) --- .env.example | 3 +++ .gitignore | 3 +++ setu-validator/src/main.rs | 43 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..50e4c6c --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# PoCW Economics +POCW_ENABLED=false +POCW_TRANSFER_FEE=21000 diff --git a/.gitignore b/.gitignore index f10e9e5..5b84d57 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ Thumbs.db **/*.rs.bk Cargo.lock +# Environment +.env + # Logs *.log logs/ diff --git a/setu-validator/src/main.rs b/setu-validator/src/main.rs index 6474be9..18fbed9 100644 --- a/setu-validator/src/main.rs +++ b/setu-validator/src/main.rs @@ -94,8 +94,51 @@ fn load_key_info(key_file: &str) -> anyhow::Result<(String, Vec, Vec)> { Ok((account_address, public_key, signature)) } +/// Load environment variables from a dotenv file. +/// Priority: `--env-file ` flag, then `.env` in the working directory. +/// Each line should be KEY=VALUE (comments and blank lines are skipped). +fn load_env_file() { + let args: Vec = std::env::args().collect(); + let explicit = args.windows(2) + .find(|w| w[0] == "--env-file") + .map(|w| w[1].clone()); + + let path = match explicit { + Some(p) => p, + None => { + // Auto-load .env from working directory if it exists + if std::path::Path::new(".env").exists() { + ".env".to_string() + } else { + return; + } + } + }; + + let content = match std::fs::read_to_string(&path) { + Ok(c) => c, + Err(e) => { + eprintln!("Failed to read env file {}: {}", path, e); + std::process::exit(1); + } + }; + + for line in content.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + if let Some((key, value)) = line.split_once('=') { + std::env::set_var(key.trim(), value.trim()); + } + } +} + #[tokio::main] async fn main() -> anyhow::Result<()> { + // Load .env automatically, or from --env-file + load_env_file(); + // Initialize tracing with more detailed output tracing_subscriber::fmt() .with_max_level(Level::DEBUG) From d9a7e615d19beaaf3a94ca43fe0d4a1812ebba11 Mon Sep 17 00:00:00 2001 From: ABresting Date: Sat, 14 Feb 2026 17:11:12 +0100 Subject: [PATCH 11/12] pocw: read PoCW config from environment variables (#8) --- setu-validator/src/main.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/setu-validator/src/main.rs b/setu-validator/src/main.rs index 18fbed9..db054ee 100644 --- a/setu-validator/src/main.rs +++ b/setu-validator/src/main.rs @@ -210,6 +210,18 @@ async fn main() -> anyhow::Result<()> { consensus_config.node_info = node_info; consensus_config.is_leader = true; // Single node mode: always leader consensus_config.consensus.validator_count = 1; // Single node mode: only 1 validator + + // PoCW configuration from environment + if std::env::var("POCW_ENABLED").unwrap_or_default() == "true" { + let fee = std::env::var("POCW_TRANSFER_FEE") + .ok().and_then(|s| s.parse().ok()).unwrap_or(21_000); + consensus_config.consensus.pocw = Some(setu_types::pocw::PoCWConfig { + enabled: true, + transfer_fee: fee, + ..Default::default() + }); + info!("✓ PoCW enabled (transfer_fee={})", fee); + } // Extract transfer fee before consensus_config is consumed let transfer_fee = consensus_config.consensus.pocw From 47147e99aca6b68750d3694c699fd0763029d154 Mon Sep 17 00:00:00 2001 From: ABresting Date: Sat, 14 Feb 2026 17:11:48 +0100 Subject: [PATCH 12/12] docs: update READMEs with --env-file usage and PoCW config (#13, #8) --- README.md | 12 +++++++++--- setu-validator/README.md | 7 ++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 204e4a5..b24fd2b 100644 --- a/README.md +++ b/README.md @@ -168,13 +168,17 @@ cargo test --all #### 1. Start Validator ```bash -# Set environment variables +# Using an env file (auto-loads .env from working directory) +./target/release/setu-validator + +# Or specify a custom env file +./target/release/setu-validator --env-file config/prod.env + +# Or set environment variables directly export VALIDATOR_ID=validator-1 export VALIDATOR_HTTP_PORT=8080 export VALIDATOR_P2P_PORT=9000 export VALIDATOR_DB_PATH=/tmp/setu/validator - -# Start validator ./target/release/setu-validator ``` @@ -229,6 +233,8 @@ docker-compose logs -f | `VALIDATOR_P2P_PORT` | `9000` | P2P network port | | `VALIDATOR_DB_PATH` | (memory) | RocksDB path for persistence | | `VALIDATOR_KEY_FILE` | - | Path to keypair file | +| `POCW_ENABLED` | `false` | Enable PoCW economic layer | +| `POCW_TRANSFER_FEE` | `21000` | Fee per transfer (burned) | ### Solver Environment Variables diff --git a/setu-validator/README.md b/setu-validator/README.md index 9a76fea..6684451 100644 --- a/setu-validator/README.md +++ b/setu-validator/README.md @@ -50,7 +50,10 @@ Validator node for the Setu network. Responsible for verifying events, maintaini # Start validator with default config cargo run -p setu-validator -# With environment variables +# With an env file (auto-loads .env from working directory, or use --env-file) +cargo run -p setu-validator -- --env-file .env + +# With inline environment variables VALIDATOR_ID=validator_1 \ VALIDATOR_PORT=8001 \ IS_LEADER=true \ @@ -83,6 +86,8 @@ validator.run().await; | node_id | VALIDATOR_ID | auto-generated | Unique validator identifier | | port | VALIDATOR_PORT | 8001 | Network port | | is_leader | IS_LEADER | false | Whether this is the leader node | +| pocw_enabled | POCW_ENABLED | false | Enable PoCW economic layer | +| pocw_transfer_fee | POCW_TRANSFER_FEE | 21000 | Fee per transfer (burned) | ## Modules