Skip to content

Commit 5cbb94c

Browse files
authored
feat(consensus): P2P consensus system with SDK trait refactor and WASM weight integration (#57)
* feat(consensus): P2P consensus system with SDK trait refactor, bootstrap period, and WASM weight integration SDK trait refactor: - Extended Challenge trait with routes(), handle_route(), get_weights(), and validate_storage_write() methods with default implementations - Updated register_challenge! macro to export all 4 new WASM functions (get_routes, handle_route, get_weights, validate_storage_write) - Added WeightEntry { uid: u16, weight: u16 } type for WASM weight serialization Bootnode and stake verification: - Added is_bootnode field to P2PConfig with with_bootnode() builder - Added is_stake_sufficient() method to ValidatorSet - Updated min validator stake to 10k TAO (10_000_000_000_000 RAO) across core/constants.rs, p2p-consensus/config.rs, subnet-manager/config.rs, and validator-node/main.rs Bootstrap period for UID 0 dominance: - Added BOOTSTRAP_PERIOD_EPOCHS (100) and BOOTSTRAP_UID0_WEIGHT (u16::MAX) constants to platform-core - Added bootstrap_active field to ChainState with is_in_bootstrap_period() - Modified finalize_weights() to override UID 0 weight during bootstrap Cross-challenge storage: - Added storage_get_cross host function allowing WASM challenges to read storage from other challenges - Added StorageBackend::get_cross() with default delegation to get() - Implemented get_cross() in ChallengeStorageBackend Sudo challenge updates: - Enhanced ChallengeUpdate handler with sudo key verification - Invalidates WASM module cache on authorized challenge updates Epoch weight calculation from WASM: - Added execute_get_weights() and execute_validate_storage_write() - Integrated WASM weight collection in CommitWindowOpen handler - Added submit_weight_vote() to ChainState for local weight submission - Added call_i32_i32_i32_i32_return_i32() for 4-arg WASM calls - Added consensus_get_subnet_challenges host function and SDK wrapper * ci: trigger CI checks
1 parent 7658918 commit 5cbb94c

14 files changed

Lines changed: 511 additions & 21 deletions

File tree

bins/validator-node/src/challenge_storage.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,12 @@ impl StorageBackend for ChallengeStorageBackend {
5454
.block_on(self.storage.delete(&storage_key))
5555
.map_err(|e| StorageHostError::StorageError(e.to_string()))
5656
}
57+
58+
fn get_cross(
59+
&self,
60+
challenge_id: &str,
61+
key: &[u8],
62+
) -> Result<Option<Vec<u8>>, StorageHostError> {
63+
self.get(challenge_id, key)
64+
}
5765
}

bins/validator-node/src/main.rs

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ async fn main() -> Result<()> {
267267
.with_listen_addr(&args.listen_addr)
268268
.with_bootstrap_peers(args.bootstrap.clone())
269269
.with_netuid(args.netuid)
270-
.with_min_stake(1_000_000_000_000); // 1000 TAO
270+
.with_min_stake(10_000_000_000_000); // 10000 TAO
271271

272272
// Initialize validator set (ourselves first)
273273
let validator_set = Arc::new(ValidatorSet::new(keypair.clone(), p2p_config.min_stake));
@@ -478,6 +478,7 @@ async fn main() -> Result<()> {
478478
&consensus,
479479
&validator_set,
480480
&state_manager,
481+
&wasm_executor,
481482
).await;
482483
}
483484

@@ -506,6 +507,8 @@ async fn main() -> Result<()> {
506507
&state_manager,
507508
netuid,
508509
version_key,
510+
&wasm_executor,
511+
&keypair,
509512
).await;
510513
}
511514

@@ -689,6 +692,7 @@ async fn handle_network_event(
689692
consensus: &Arc<RwLock<ConsensusEngine>>,
690693
validator_set: &Arc<ValidatorSet>,
691694
state_manager: &Arc<StateManager>,
695+
wasm_executor_ref: &Option<Arc<WasmChallengeExecutor>>,
692696
) {
693697
match event {
694698
NetworkEvent::Message { source, message } => match message {
@@ -969,13 +973,27 @@ async fn handle_network_event(
969973
);
970974
}
971975
P2PMessage::ChallengeUpdate(update) => {
972-
info!(
973-
challenge_id = %update.challenge_id,
974-
updater = %update.updater.to_hex(),
975-
update_type = %update.update_type,
976-
data_bytes = update.data.len(),
977-
"Received challenge update"
978-
);
976+
let updater_ss58 = update.updater.to_hex();
977+
if updater_ss58 == platform_p2p_consensus::SUDO_HOTKEY
978+
|| update.updater.0 == platform_core::SUDO_KEY_BYTES
979+
{
980+
info!(
981+
challenge_id = %update.challenge_id,
982+
updater = %updater_ss58,
983+
update_type = %update.update_type,
984+
data_bytes = update.data.len(),
985+
"Received authorized challenge update from sudo key"
986+
);
987+
if let Some(ref executor) = wasm_executor_ref {
988+
executor.invalidate_cache(&update.challenge_id.to_string());
989+
}
990+
} else {
991+
warn!(
992+
challenge_id = %update.challenge_id,
993+
updater = %updater_ss58,
994+
"Rejected challenge update from non-sudo key"
995+
);
996+
}
979997
}
980998
P2PMessage::StorageProposal(proposal) => {
981999
debug!(
@@ -1059,6 +1077,8 @@ async fn handle_block_event(
10591077
state_manager: &Arc<StateManager>,
10601078
netuid: u16,
10611079
version_key: u64,
1080+
wasm_executor: &Option<Arc<WasmChallengeExecutor>>,
1081+
keypair: &Keypair,
10621082
) {
10631083
match event {
10641084
BlockSyncEvent::NewBlock { block_number, .. } => {
@@ -1089,6 +1109,45 @@ async fn handle_block_event(
10891109
epoch, block
10901110
);
10911111

1112+
// Collect WASM-computed weights from challenges before finalizing
1113+
if let Some(ref executor) = wasm_executor {
1114+
let challenges: Vec<String> = state_manager
1115+
.apply(|state| state.challenges.keys().map(|k| k.to_string()).collect());
1116+
let local_hotkey = keypair.hotkey();
1117+
for cid in &challenges {
1118+
match executor.execute_get_weights(cid) {
1119+
Ok(weights) if !weights.is_empty() => {
1120+
state_manager.apply(|state| {
1121+
if let Err(e) = state.submit_weight_vote(
1122+
local_hotkey.clone(),
1123+
netuid,
1124+
weights.clone(),
1125+
) {
1126+
warn!(
1127+
challenge_id = %cid,
1128+
error = %e,
1129+
"Failed to submit WASM-computed weights"
1130+
);
1131+
}
1132+
});
1133+
info!(
1134+
challenge_id = %cid,
1135+
weight_count = weights.len(),
1136+
"Integrated WASM-computed weights"
1137+
);
1138+
}
1139+
Ok(_) => {}
1140+
Err(e) => {
1141+
debug!(
1142+
challenge_id = %cid,
1143+
error = %e,
1144+
"WASM get_weights not available for challenge"
1145+
);
1146+
}
1147+
}
1148+
}
1149+
}
1150+
10921151
// Get weights from decentralized state
10931152
if let (Some(st), Some(sig)) = (subtensor.as_ref(), signer.as_ref()) {
10941153
let final_weights = state_manager.apply(|state| state.finalize_weights());

bins/validator-node/src/wasm_executor.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,116 @@ impl WasmChallengeExecutor {
793793
Ok((result_data, metrics))
794794
}
795795

796+
pub fn execute_get_weights(&self, module_path: &str) -> Result<Vec<(u16, u16)>> {
797+
let start = Instant::now();
798+
799+
let module = self
800+
.load_module(module_path)
801+
.context("Failed to load WASM module")?;
802+
803+
let network_host_fns = Arc::new(NetworkHostFunctions::all());
804+
805+
let instance_config = InstanceConfig {
806+
challenge_id: module_path.to_string(),
807+
validator_id: "validator".to_string(),
808+
storage_host_config: StorageHostConfig {
809+
allow_direct_writes: true,
810+
require_consensus: false,
811+
..self.config.storage_host_config.clone()
812+
},
813+
storage_backend: Arc::clone(&self.config.storage_backend),
814+
consensus_policy: ConsensusPolicy::read_only(),
815+
..Default::default()
816+
};
817+
818+
let mut instance = self
819+
.runtime
820+
.instantiate(&module, instance_config, Some(network_host_fns))
821+
.map_err(|e| anyhow::anyhow!("WASM instantiation failed: {}", e))?;
822+
823+
let result = instance
824+
.call_return_i64("get_weights")
825+
.map_err(|e| anyhow::anyhow!("WASM get_weights call failed: {}", e))?;
826+
827+
let out_len = (result >> 32) as i32;
828+
let out_ptr = (result & 0xFFFF_FFFF) as i32;
829+
830+
let result_data = if out_ptr > 0 && out_len > 0 {
831+
instance
832+
.read_memory(out_ptr as usize, out_len as usize)
833+
.map_err(|e| {
834+
anyhow::anyhow!("failed to read WASM memory for get_weights output: {}", e)
835+
})?
836+
} else {
837+
return Ok(Vec::new());
838+
};
839+
840+
let weights: Vec<(u16, u16)> = bincode::DefaultOptions::new()
841+
.with_fixint_encoding()
842+
.allow_trailing_bytes()
843+
.with_limit(MAX_ROUTE_OUTPUT_SIZE)
844+
.deserialize(&result_data)
845+
.context("Failed to deserialize get_weights output")?;
846+
847+
info!(
848+
module = module_path,
849+
weight_count = weights.len(),
850+
execution_time_ms = start.elapsed().as_millis() as u64,
851+
"WASM get_weights completed"
852+
);
853+
854+
Ok(weights)
855+
}
856+
857+
#[allow(dead_code)]
858+
pub fn execute_validate_storage_write(
859+
&self,
860+
module_path: &str,
861+
key: &[u8],
862+
value: &[u8],
863+
) -> Result<bool> {
864+
let module = self
865+
.load_module(module_path)
866+
.context("Failed to load WASM module")?;
867+
868+
let network_host_fns = Arc::new(NetworkHostFunctions::all());
869+
870+
let instance_config = InstanceConfig {
871+
challenge_id: module_path.to_string(),
872+
validator_id: "validator".to_string(),
873+
storage_host_config: StorageHostConfig::default(),
874+
storage_backend: Arc::clone(&self.config.storage_backend),
875+
..Default::default()
876+
};
877+
878+
let mut instance = self
879+
.runtime
880+
.instantiate(&module, instance_config, Some(network_host_fns))
881+
.map_err(|e| anyhow::anyhow!("WASM instantiation failed: {}", e))?;
882+
883+
let key_ptr = self.allocate_input(&mut instance, key)?;
884+
instance
885+
.write_memory(key_ptr as usize, key)
886+
.map_err(|e| anyhow::anyhow!("Failed to write key to WASM memory: {}", e))?;
887+
888+
let val_ptr = self.allocate_input(&mut instance, value)?;
889+
instance
890+
.write_memory(val_ptr as usize, value)
891+
.map_err(|e| anyhow::anyhow!("Failed to write value to WASM memory: {}", e))?;
892+
893+
let result = instance
894+
.call_i32_i32_i32_i32_return_i32(
895+
"validate_storage_write",
896+
key_ptr,
897+
key.len() as i32,
898+
val_ptr,
899+
value.len() as i32,
900+
)
901+
.map_err(|e| anyhow::anyhow!("WASM validate_storage_write call failed: {}", e))?;
902+
903+
Ok(result == 1)
904+
}
905+
796906
fn load_module(&self, module_path: &str) -> Result<Arc<WasmModule>> {
797907
{
798908
let cache = self.module_cache.read();

crates/challenge-sdk-wasm/src/host_functions.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ extern "C" {
1616
extern "C" {
1717
fn storage_get(key_ptr: i32, key_len: i32, value_ptr: i32) -> i32;
1818
fn storage_set(key_ptr: i32, key_len: i32, value_ptr: i32, value_len: i32) -> i32;
19+
fn storage_get_cross(
20+
cid_ptr: i32,
21+
cid_len: i32,
22+
key_ptr: i32,
23+
key_len: i32,
24+
value_ptr: i32,
25+
) -> i32;
1926
}
2027

2128
#[link(wasm_import_module = "platform_terminal")]
@@ -110,6 +117,24 @@ pub fn host_storage_set(key: &[u8], value: &[u8]) -> Result<(), i32> {
110117
Ok(())
111118
}
112119

120+
pub fn host_storage_get_cross(challenge_id: &[u8], key: &[u8]) -> Result<Vec<u8>, i32> {
121+
let mut value_buf = vec![0u8; RESPONSE_BUF_MEDIUM];
122+
let status = unsafe {
123+
storage_get_cross(
124+
challenge_id.as_ptr() as i32,
125+
challenge_id.len() as i32,
126+
key.as_ptr() as i32,
127+
key.len() as i32,
128+
value_buf.as_mut_ptr() as i32,
129+
)
130+
};
131+
if status < 0 {
132+
return Err(status);
133+
}
134+
value_buf.truncate(status as usize);
135+
Ok(value_buf)
136+
}
137+
113138
pub fn host_terminal_exec(request: &[u8]) -> Result<Vec<u8>, i32> {
114139
let mut result_buf = vec![0u8; RESPONSE_BUF_LARGE];
115140
let status = unsafe {
@@ -256,6 +281,7 @@ extern "C" {
256281
fn consensus_get_state_hash(buf_ptr: i32) -> i32;
257282
fn consensus_get_submission_count() -> i32;
258283
fn consensus_get_block_height() -> i64;
284+
fn consensus_get_subnet_challenges(buf_ptr: i32, buf_len: i32) -> i32;
259285
}
260286

261287
pub fn host_consensus_get_epoch() -> i64 {
@@ -306,3 +332,14 @@ pub fn host_consensus_get_submission_count() -> i32 {
306332
pub fn host_consensus_get_block_height() -> i64 {
307333
unsafe { consensus_get_block_height() }
308334
}
335+
336+
pub fn host_consensus_get_subnet_challenges() -> Result<Vec<u8>, i32> {
337+
let mut buf = vec![0u8; RESPONSE_BUF_MEDIUM];
338+
let status =
339+
unsafe { consensus_get_subnet_challenges(buf.as_mut_ptr() as i32, buf.len() as i32) };
340+
if status < 0 {
341+
return Err(status);
342+
}
343+
buf.truncate(status as usize);
344+
Ok(buf)
345+
}

crates/challenge-sdk-wasm/src/lib.rs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub use types::{
1313
};
1414
pub use types::{ContainerRunRequest, ContainerRunResponse};
1515
pub use types::{EvaluationInput, EvaluationOutput};
16-
pub use types::{WasmRouteDefinition, WasmRouteRequest, WasmRouteResponse};
16+
pub use types::{WasmRouteDefinition, WasmRouteRequest, WasmRouteResponse, WeightEntry};
1717

1818
pub trait Challenge {
1919
fn name(&self) -> &'static str;
@@ -49,6 +49,19 @@ pub trait Challenge {
4949
fn handle_route(&self, _request: &[u8]) -> alloc::vec::Vec<u8> {
5050
alloc::vec::Vec::new()
5151
}
52+
53+
/// Return serialized epoch weight entries (`Vec<WeightEntry>`) that the
54+
/// validator should set on-chain. The default implementation returns an
55+
/// empty vector (no weights).
56+
fn get_weights(&self) -> alloc::vec::Vec<u8> {
57+
alloc::vec::Vec::new()
58+
}
59+
60+
/// Validate whether a storage write with the given `key` and `value` is
61+
/// permitted. The default implementation allows all writes.
62+
fn validate_storage_write(&self, _key: &[u8], _value: &[u8]) -> bool {
63+
true
64+
}
5265
}
5366

5467
/// Pack a pointer and length into a single i64 value.
@@ -264,5 +277,39 @@ macro_rules! register_challenge {
264277
}
265278
$crate::pack_ptr_len(ptr as i32, output.len() as i32)
266279
}
280+
281+
#[no_mangle]
282+
pub extern "C" fn get_weights() -> i64 {
283+
let output = <$ty as $crate::Challenge>::get_weights(&_CHALLENGE);
284+
if output.is_empty() {
285+
return $crate::pack_ptr_len(0, 0);
286+
}
287+
let ptr = $crate::alloc_impl::sdk_alloc(output.len());
288+
if ptr.is_null() {
289+
return $crate::pack_ptr_len(0, 0);
290+
}
291+
unsafe {
292+
core::ptr::copy_nonoverlapping(output.as_ptr(), ptr, output.len());
293+
}
294+
$crate::pack_ptr_len(ptr as i32, output.len() as i32)
295+
}
296+
297+
#[no_mangle]
298+
pub extern "C" fn validate_storage_write(
299+
key_ptr: i32,
300+
key_len: i32,
301+
val_ptr: i32,
302+
val_len: i32,
303+
) -> i32 {
304+
let key =
305+
unsafe { core::slice::from_raw_parts(key_ptr as *const u8, key_len as usize) };
306+
let value =
307+
unsafe { core::slice::from_raw_parts(val_ptr as *const u8, val_len as usize) };
308+
if <$ty as $crate::Challenge>::validate_storage_write(&_CHALLENGE, key, value) {
309+
1
310+
} else {
311+
0
312+
}
313+
}
267314
};
268315
}

crates/challenge-sdk-wasm/src/types.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,13 @@ pub struct WasmRouteResponse {
189189
/// Raw response body bytes.
190190
pub body: Vec<u8>,
191191
}
192+
193+
/// A single weight entry mapping a UID to a weight value.
194+
///
195+
/// Returned by [`Challenge::get_weights`] as a serialized `Vec<WeightEntry>`.
196+
/// Both fields use `u16` to match the on-chain weight vector format.
197+
#[derive(Clone, Debug, Serialize, Deserialize)]
198+
pub struct WeightEntry {
199+
pub uid: u16,
200+
pub weight: u16,
201+
}

0 commit comments

Comments
 (0)