Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions agent/flow-trace/03_E3_REQUEST_AND_COMMITTEE.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ CiphernodeRegistrySolWriter receives CommitteeFinalizeRequested
│ │ │ │ } │ │
│ │ │ └──────────────────────────────────────────┘ │
│ │ │
│ │ 6. Emit CommitteeFinalized(e3Id, committee)
│ │ 6. Emit CommitteeFinalized(e3Id, committee, scores)
│ │ } │
│ └─────────────────────────────────────────────────────────┘
```
Expand All @@ -303,7 +303,7 @@ CiphernodeRegistrySolWriter receives CommitteeFinalizeRequested
CiphernodeRegistrySolReader decodes CommitteeFinalized event
├─ Publishes EnclaveEvent::CommitteeFinalized {
│ e3_id, committee: [addr1, addr2, ..., addrN], chain_id
│ e3_id, committee: [addr1, addr2, ..., addrN], scores: [s1, s2, ..., sN], chain_id
│ }
├─ Sortition actor:
Expand Down
32 changes: 32 additions & 0 deletions crates/events/src/enclave_event/committee_finalized.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use crate::E3id;
use actix::Message;
use alloy::primitives::U256;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display};

Expand All @@ -14,9 +15,40 @@ use std::fmt::{self, Display};
pub struct CommitteeFinalized {
pub e3_id: E3id,
pub committee: Vec<String>,
pub scores: Vec<String>,
pub chain_id: u64,
}

impl CommitteeFinalized {
/// Sort committee members by ascending score so every node derives the same
/// deterministic ordering. The node with the lowest score ends up at index 0.
/// If scores are empty or unparseable, the order is left unchanged.
pub fn sort_by_score(&mut self) {
if self.scores.len() != self.committee.len() || self.scores.is_empty() {
return;
}

// Build (index, parsed_score) pairs
let mut indices: Vec<usize> = (0..self.committee.len()).collect();
let parsed: Vec<Option<U256>> =
self.scores.iter().map(|s| s.parse::<U256>().ok()).collect();

// If any score fails to parse, leave order unchanged
if parsed.iter().any(|s| s.is_none()) {
return;
}

indices.sort_by_key(|&i| parsed[i].unwrap());

let sorted_committee: Vec<String> =
indices.iter().map(|&i| self.committee[i].clone()).collect();
let sorted_scores: Vec<String> = indices.iter().map(|&i| self.scores[i].clone()).collect();

self.committee = sorted_committee;
self.scores = sorted_scores;
}
}

impl Display for CommitteeFinalized {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
Expand Down
7 changes: 5 additions & 2 deletions crates/evm/src/ciphernode_registry_sol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,19 @@ struct CommitteeFinalizedWithChainId(pub ICiphernodeRegistry::CommitteeFinalized

impl From<CommitteeFinalizedWithChainId> for CommitteeFinalized {
fn from(value: CommitteeFinalizedWithChainId) -> Self {
e3_events::CommitteeFinalized {
let mut result = e3_events::CommitteeFinalized {
e3_id: E3id::new(value.0.e3Id.to_string(), value.1),
committee: value
.0
.committee
.iter()
.map(|addr| addr.to_string())
.collect(),
scores: value.0.scores.iter().map(|s| s.to_string()).collect(),
chain_id: value.1,
}
};
result.sort_by_score();
result
}
}

Expand Down
32 changes: 28 additions & 4 deletions crates/tests/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,20 @@ pub fn save_snapshot(file_name: &str, bytes: &[u8]) {
fs::write(format!("tests/{file_name}"), bytes).unwrap();
}

/// Compute placeholder scores for a committee.
/// Uses ticket_id=0 for each address with the given e3_id and seed.
fn compute_committee_scores(committee: &[String], e3_id: &E3id, seed: Seed) -> Vec<String> {
use e3_sortition::hash_to_score;
committee
.iter()
.map(|addr| {
let address: Address = addr.parse().unwrap();
let score = hash_to_score(address, 0, e3_id.clone(), seed);
U256::from_be_slice(&score.to_bytes_be()).to_string()
})
.collect()
}

/// Determines the committee for a given E3 request using deterministic sortition.
///
/// This function runs the same sortition algorithm that the ciphernodes use internally,
Expand All @@ -415,15 +429,15 @@ pub fn save_snapshot(file_name: &str, bytes: &[u8]) {
/// * `collector_addr` - Address of the collector node (for validation)
///
/// # Returns
/// A tuple of (committee_addresses, buffer_addresses)
/// A tuple of (committee_addresses, committee_scores, buffer_addresses)
fn determine_committee(
e3_id: &E3id,
seed: Seed,
threshold_m: usize,
threshold_n: usize,
registered_addrs: &[String],
collector_addr: &str,
) -> Result<(Vec<String>, Vec<String>)> {
) -> Result<(Vec<String>, Vec<String>, Vec<String>)> {
let buffer = calculate_buffer_size(threshold_m, threshold_n);
let total_selection_size = threshold_n + buffer;

Expand Down Expand Up @@ -457,6 +471,12 @@ fn determine_committee(
.map(|w| w.address.to_string())
.collect();

let committee_scores: Vec<String> = winners
.iter()
.take(threshold_n)
.map(|w| U256::from_be_slice(&w.score.to_bytes_be()).to_string())
.collect();

let buffer_nodes: Vec<String> = winners
.iter()
.skip(threshold_n)
Expand All @@ -476,7 +496,7 @@ fn determine_committee(
}
}

Ok((committee, buffer_nodes))
Ok((committee, committee_scores, buffer_nodes))
}

async fn setup_score_sortition_environment(
Expand Down Expand Up @@ -736,7 +756,7 @@ async fn test_trbfv_actor() -> Result<()> {

sleep(Duration::from_millis(500)).await;

let (committee, buffer_nodes) = determine_committee(
let (committee, committee_scores, buffer_nodes) = determine_committee(
&e3_id,
seed,
threshold_m,
Expand All @@ -756,6 +776,7 @@ async fn test_trbfv_actor() -> Result<()> {
bus.publish_without_context(CommitteeFinalized {
e3_id: e3_id.clone(),
committee: committee.clone(),
scores: committee_scores,
chain_id,
})?;

Expand Down Expand Up @@ -1252,6 +1273,7 @@ async fn test_stopped_keyshares_retain_state() -> Result<()> {
bus.publish_without_context(CommitteeFinalized {
e3_id: e3_id.clone(),
committee: eth_addrs.clone(),
scores: compute_committee_scores(&eth_addrs, &e3_id, seed.clone()),
chain_id: 1,
})?;

Expand Down Expand Up @@ -1499,6 +1521,7 @@ async fn test_duplicate_e3_id_with_different_chain_id() -> Result<()> {
bus.publish_without_context(CommitteeFinalized {
e3_id: E3id::new("1234", 1),
committee: eth_addrs.clone(),
scores: compute_committee_scores(&eth_addrs, &E3id::new("1234", 1), seed.clone()),
chain_id: 1,
})?;

Expand Down Expand Up @@ -1537,6 +1560,7 @@ async fn test_duplicate_e3_id_with_different_chain_id() -> Result<()> {
bus.publish_without_context(CommitteeFinalized {
e3_id: E3id::new("1234", 2),
committee: eth_addrs.clone(),
scores: compute_committee_scores(&eth_addrs, &E3id::new("1234", 2), seed.clone()),
chain_id: 2,
})?;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2565,5 +2565,5 @@
"deployedLinkReferences": {},
"immutableReferences": {},
"inputSourceName": "project/contracts/Enclave.sol",
"buildInfoId": "solc-0_8_28-74ea8e366bd07fddf0065d16be962bc1ae5e80d7"
"buildInfoId": "solc-0_8_28-d3db7173950582d145915864c28ba15e975a7c98"
}
Original file line number Diff line number Diff line change
Expand Up @@ -940,5 +940,5 @@
"deployedLinkReferences": {},
"immutableReferences": {},
"inputSourceName": "project/contracts/interfaces/IBondingRegistry.sol",
"buildInfoId": "solc-0_8_28-74ea8e366bd07fddf0065d16be962bc1ae5e80d7"
"buildInfoId": "solc-0_8_28-d3db7173950582d145915864c28ba15e975a7c98"
}
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@
"internalType": "address[]",
"name": "committee",
"type": "address[]"
},
{
"indexed": false,
"internalType": "uint256[]",
"name": "scores",
"type": "uint256[]"
}
],
"name": "CommitteeFinalized",
Expand Down Expand Up @@ -954,5 +960,5 @@
"deployedLinkReferences": {},
"immutableReferences": {},
"inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol",
"buildInfoId": "solc-0_8_28-74ea8e366bd07fddf0065d16be962bc1ae5e80d7"
"buildInfoId": "solc-0_8_28-d3db7173950582d145915864c28ba15e975a7c98"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2093,5 +2093,5 @@
"deployedLinkReferences": {},
"immutableReferences": {},
"inputSourceName": "project/contracts/interfaces/IEnclave.sol",
"buildInfoId": "solc-0_8_28-74ea8e366bd07fddf0065d16be962bc1ae5e80d7"
"buildInfoId": "solc-0_8_28-d3db7173950582d145915864c28ba15e975a7c98"
}
Original file line number Diff line number Diff line change
Expand Up @@ -954,5 +954,5 @@
"deployedLinkReferences": {},
"immutableReferences": {},
"inputSourceName": "project/contracts/interfaces/ISlashingManager.sol",
"buildInfoId": "solc-0_8_28-74ea8e366bd07fddf0065d16be962bc1ae5e80d7"
"buildInfoId": "solc-0_8_28-d3db7173950582d145915864c28ba15e975a7c98"
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,12 @@ interface ICiphernodeRegistry {
/// @notice This event MUST be emitted when a committee is finalized
/// @param e3Id ID of the E3 computation
/// @param committee Array of selected ciphernode addresses
event CommitteeFinalized(uint256 indexed e3Id, address[] committee);
/// @param scores Array of sortition scores corresponding to each committee member
event CommitteeFinalized(
uint256 indexed e3Id,
address[] committee,
uint256[] scores
);

/// @notice This event MUST be emitted when committee formation fails (threshold not met)
/// @param e3Id ID of the E3 computation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,14 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable {
c.stage = ICiphernodeRegistry.CommitteeStage.Finalized;
c.activeCount = c.topNodes.length;

uint256 len = c.topNodes.length;
uint256[] memory scores = new uint256[](len);
for (uint256 i = 0; i < len; ++i) {
scores[i] = c.scoreOf[c.topNodes[i]];
}

enclave.onCommitteeFinalized(e3Id);
emit CommitteeFinalized(e3Id, c.topNodes);
emit CommitteeFinalized(e3Id, c.topNodes, scores);
return true;
}

Expand Down
3 changes: 2 additions & 1 deletion packages/enclave-sdk/src/events/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ export interface CommitteePublishedData {

export interface CommitteeFinalizedData {
e3Id: bigint
nodes: string[]
committee: string[]
scores: bigint[]
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export interface EnclaveEventData {
Expand Down
Loading