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
37 changes: 21 additions & 16 deletions crates/evm/src/ciphernode_registry_sol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,22 +462,27 @@ pub async fn finalize_committee_on_registry<P: Provider + WalletProvider + Clone

// 0x5e043d1a = SubmissionWindowNotClosed(),
// 0xd4c1d970 = CommitteeNotRequested()
send_tx_with_retry("finalizeCommittee", &["0x5e043d1a", "0xd4c1d970"], || {
let provider = provider.clone();
async move {
info!("Calling: contract.finalizeCommittee(..)");
let from_address = provider.provider().default_signer_address();
let current_nonce = provider
.provider()
.get_transaction_count(from_address)
.pending()
.await?;
let contract = ICiphernodeRegistry::new(contract_address, provider.provider());
let builder = contract.finalizeCommittee(e3_id_u256).nonce(current_nonce);
let receipt = builder.send().await?.get_receipt().await?;
Ok(receipt)
}
})
// 0x59fa4a93 = ThresholdNotMet()
send_tx_with_retry(
"finalizeCommittee",
&["0x5e043d1a", "0xd4c1d970", "0x59fa4a93"],
|| {
let provider = provider.clone();
async move {
info!("Calling: contract.finalizeCommittee(..)");
let from_address = provider.provider().default_signer_address();
let current_nonce = provider
.provider()
.get_transaction_count(from_address)
.pending()
.await?;
let contract = ICiphernodeRegistry::new(contract_address, provider.provider());
let builder = contract.finalizeCommittee(e3_id_u256).nonce(current_nonce);
let receipt = builder.send().await?.get_receipt().await?;
Ok(receipt)
}
},
)
.await
}

Expand Down
3 changes: 1 addition & 2 deletions examples/CRISP/client/src/model/poll.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ export interface PollResult {

export interface PollRequestResult {
round_id: number
option_1_tally: number
option_2_tally: number
tally: number[]
option_1_emoji: string
option_2_emoji: string
end_time: number
Expand Down
4 changes: 2 additions & 2 deletions examples/CRISP/client/src/utils/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ export const convertPollData = (request: PollRequestResult[]): PollResult[] => {
const options: PollOption[] = [
{
value: 0,
votes: poll.option_1_tally,
votes: poll.tally[0] ?? 0,
label: poll.option_1_emoji,
checked: false,
},
{
value: 1,
votes: poll.option_2_tally,
votes: poll.tally[1] ?? 0,
label: poll.option_2_emoji,
checked: false,
},
Expand Down
148 changes: 119 additions & 29 deletions examples/CRISP/crates/crisp-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,50 +8,128 @@ use e3_bfv_client::decode_bytes_to_vec_u64;
use eyre::Result;
use num_bigint::BigUint;

/// Maximum number of bits that can fit each vote option.
pub const MAX_VOTE_BITS: usize = 50;

/// Represents decoded vote counts from a tally
#[derive(Debug, Clone)]
pub struct VoteCounts {
pub yes: BigUint,
pub no: BigUint,
}

/// Decode an encoded tally into its decimal representation.
/// Decode an FHE-encrypted tally result into vote counts for each choice.
///
/// # Encoding scheme
///
/// The BFV plaintext polynomial has `degree` coefficients (e.g., 512).
/// When encoding a vote with `n` choices, the polynomial is divided into
/// `n` equal segments of `floor(degree / n)` coefficients each.
///
/// Each segment represents one choice's vote count in binary (big-endian):
///
/// |---- segment 0 ----|---- segment 1 ----|---- segment 2 ----|-- padding --|
/// | choice 0 (Yes) | choice 1 (No) | choice 2 (Abst) | zeros |
/// | binary, MSB first| binary, MSB first | binary, MSB first| |
///
/// Within each segment, the binary representation is right-aligned:
/// [0, 0, 0, ..., 0, 1, 0, 1, 1] ← represents decimal 11
/// ^-- leading zeros ^-- MSB ^-- LSB
///
/// Because FHE addition is performed coefficient-wise on the polynomial,
/// summing N encrypted votes produces the total count per coefficient.
/// The binary reconstruction then recovers the final tally per choice.
///
/// # MAX_VOTE_BITS
///
/// The plaintext output from FHE computation contains the result as bytes.
/// Votes are encoded with yes votes in the first half and no votes in the second half,
/// right-aligned with leading zeros.
/// To prevent overflow during FHE computation, only the last `MAX_VOTE_BITS`
/// coefficients of each segment are used (MAX_VOTE_BITS = 50). This caps the
/// maximum representable vote count at `2^50 - 1` (~1.1 quadrillion).
///
/// We read from the right side of each segment (the significant bits)
/// and ignore leading zeros on the left.
///
/// # Arguments
///
/// * `tally_bytes` - The encoded tally as bytes (little-endian format of u64s)
/// * `tally_bytes` - Raw bytes from the FHE decryption, encoding u64 values
/// in little-endian format (8 bytes per coefficient).
/// * `num_choices` - Number of voting options (must match what was used to encode).
///
/// # Returns
///
/// A `VoteCounts` struct containing the decoded yes and no vote counts
pub fn decode_tally(tally_bytes: &[u8]) -> Result<VoteCounts> {
// Decode bytes to numbers array (little-endian, 8 bytes per value)
let decoded = decode_bytes_to_vec_u64(tally_bytes)?;

// Votes are right-aligned with leading zeros, so we can use the entire halves
let half_d = decoded.len() / 2;
let yes_binary = &decoded[0..half_d];
let no_binary = &decoded[half_d..decoded.len()];

// Convert yes votes (entire first half)
let mut yes = BigUint::from(0u64);
for (i, &value) in yes_binary.iter().enumerate() {
let weight = BigUint::from(2u64).pow((yes_binary.len() - 1 - i) as u32);
yes += BigUint::from(value) * weight;
/// A `Vec<BigUint>` of length `num_choices`, where `results[i]` is the
/// total vote weight for choice `i`.
///
/// # Example
///
/// Given degree=512, MAX_VOTE_BITS=50, num_choices=2:
/// segment_size = 512 / 2 = 256 coefficients per choice
/// effective_size = min(256, 50) = 50
///
/// Choice 0 reads coefficients [206..256) (last 50 of segment 0)
/// Choice 1 reads coefficients [462..512) (last 50 of segment 1)
///
/// Given degree=512, MAX_VOTE_BITS=50, num_choices=4:
/// segment_size = 512 / 4 = 128 coefficients per choice
/// effective_size = min(128, 50) = 50
/// remainder = 512 - (128 * 4) = 0
///
/// Choice 0 reads coefficients [ 78..128) (last 50 of segment 0)
/// Choice 1 reads coefficients [206..256) (last 50 of segment 1)
/// Choice 2 reads coefficients [334..384) (last 50 of segment 2)
/// Choice 3 reads coefficients [462..512) (last 50 of segment 3)
///
/// Given degree=512, MAX_VOTE_BITS=50, num_choices=3:
/// segment_size = 512 / 3 = 170 coefficients per choice
/// effective_size = min(170, 50) = 50
/// remainder = 512 - (170 * 3) = 2 coefficients (trailing zeros, ignored)
///
/// Choice 0 reads coefficients [120..170)
/// Choice 1 reads coefficients [290..340)
/// Choice 2 reads coefficients [460..510)
///
pub fn decode_tally(tally_bytes: &[u8], num_choices: usize) -> Result<Vec<BigUint>> {
if num_choices == 0 {
return Err(eyre::eyre!("Number of choices must be positive"));
}

// Convert no votes (entire second half)
let mut no = BigUint::from(0u64);
for (i, &value) in no_binary.iter().enumerate() {
let weight = BigUint::from(2u64).pow((no_binary.len() - 1 - i) as u32);
no += BigUint::from(value) * weight;
// Each u64 coefficient is stored as 8 little-endian bytes.
// This gives us the full polynomial coefficient array.
let values = decode_bytes_to_vec_u64(tally_bytes)?;

// Divide the polynomial evenly into num_choices segments.
// Any leftover coefficients (degree % num_choices) are trailing
// zeros and are ignored.
let segment_size = values.len() / num_choices;
Comment thread
ctrlc03 marked this conversation as resolved.

// Only read the rightmost MAX_VOTE_BITS (50) coefficients from each
// segment to avoid overflow. If the segment is smaller than
// MAX_VOTE_BITS (unlikely with degree=512), use the full segment.
let effective_size = segment_size.min(MAX_VOTE_BITS);

let mut results = Vec::with_capacity(num_choices);

for choice_idx in 0..num_choices {
// Find where this choice's segment starts in the array
let segment_start = choice_idx * segment_size;

// Right-align: skip leading zeros, read only the significant bits
// at the end of the segment
let read_start = segment_start + segment_size - effective_size;
let segment = &values[read_start..read_start + effective_size];

// Reconstruct the vote count from binary (big-endian within segment):
// value = segment[0] * 2^(n-1) + segment[1] * 2^(n-2) + ... + segment[n-1] * 2^0
let mut value = BigUint::from(0u64);
for (i, &v) in segment.iter().enumerate() {
let weight = BigUint::from(2u64).pow((segment.len() - 1 - i) as u32);
value += BigUint::from(v) * weight;
}

results.push(value);
}

Ok(VoteCounts { yes, no })
Ok(results)
}
Comment thread
ctrlc03 marked this conversation as resolved.

#[cfg(test)]
Expand All @@ -64,9 +142,21 @@ mod tests {
let tally_hex = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000100000000000000010000000000000001000000000000000100000000000000010000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000300000000000000000000000000000003000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000030000000000000003000000000000000300000000000000030000000000000003000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";

let bytes = hex::decode(tally_hex.strip_prefix("0x").unwrap_or(tally_hex)).unwrap();
let result = decode_tally(&bytes).unwrap();
let result = decode_tally(&bytes, 2).unwrap();

assert_eq!(result[0], BigUint::from(10000000000u64));
assert_eq!(result[1], BigUint::from(30000000000u64));
}

#[test]
fn test_decode_tally_with_wrong_num_options() {
// Expected: yes = 10000000000, no = 30000000000
let tally_hex = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000100000000000000010000000000000001000000000000000100000000000000010000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000300000000000000000000000000000003000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000030000000000000003000000000000000300000000000000030000000000000003000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";

let bytes = hex::decode(tally_hex.strip_prefix("0x").unwrap_or(tally_hex)).unwrap();
let result = decode_tally(&bytes, 3).unwrap();

assert_eq!(result.yes, BigUint::from(10000000000u64));
assert_eq!(result.no, BigUint::from(30000000000u64));
assert!(result[0] != BigUint::from(10000000000u64));
assert!(result[1] != BigUint::from(30000000000u64));
}
}
2 changes: 1 addition & 1 deletion examples/CRISP/packages/crisp-contracts/deploy/crisp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export const deployCRISPContracts = async () => {
address: tokenAddress,
blockNumber: await ethers.provider.getBlockNumber(),
},
'MockCRISPToken',
'MockVotingToken',
chain,
)
}
Expand Down
19 changes: 8 additions & 11 deletions examples/CRISP/server/src/server/indexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,20 +348,17 @@ pub async fn register_plaintext_output_published(
async move {
info!("[e3_id={}] Handling PlaintextOutputPublished", e3_id);

let num_options = repo.get_num_options().await?;

// The plaintextOutput from the event contains the result of the FHE computation.
// Decode the tally using the utility function.
let vote_counts = decode_tally(&event.plaintextOutput)?;

info!(
"[e3_id={}] Votes Option 1 (Yes): {:?}",
e3_id, vote_counts.yes
);
info!(
"[e3_id={}] Votes Option 2 (No): {:?}",
e3_id, vote_counts.no
);
let vote_counts = decode_tally(&event.plaintextOutput, num_options)?;

for (i, count) in vote_counts.iter().enumerate() {
info!("[e3_id={}] Option index: {} votes: {:?}", e3_id, i, count);
}

repo.set_votes(vote_counts.yes, vote_counts.no).await?;
repo.set_votes(vote_counts).await?;
repo.update_status("Finished").await?;
Ok(())
}
Expand Down
12 changes: 4 additions & 8 deletions examples/CRISP/server/src/server/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,7 @@ pub struct RoundRequest {
#[derive(Debug, Deserialize, Serialize)]
pub struct WebResultRequest {
pub round_id: u64,
pub option_1_tally: String,
pub option_2_tally: String,
pub tally: Vec<String>,
pub option_1_emoji: String,
pub option_2_emoji: String,
pub total_votes: u64,
Expand Down Expand Up @@ -206,8 +205,7 @@ pub struct E3 {
pub status: String,
pub has_voted: Vec<String>,
pub vote_count: u64,
pub votes_option_1: String,
pub votes_option_2: String,
pub tally: Vec<String>,

// Timing-related
pub start_time: u64,
Expand Down Expand Up @@ -239,8 +237,7 @@ pub struct E3Crisp {
pub has_voted: Vec<String>,
pub start_time: u64,
pub status: String,
pub votes_option_1: String,
pub votes_option_2: String,
pub tally: Vec<String>,
pub token_holder_hashes: Vec<String>,
pub eligible_addresses: Vec<TokenHolder>,
pub token_address: String,
Expand All @@ -256,8 +253,7 @@ impl From<E3> for WebResultRequest {
fn from(e3: E3) -> Self {
WebResultRequest {
round_id: e3.id,
option_1_tally: e3.votes_option_1,
option_2_tally: e3.votes_option_2,
tally: e3.tally,
option_1_emoji: e3.emojis[0].clone(),
option_2_emoji: e3.emojis[1].clone(),
total_votes: e3.vote_count,
Expand Down
37 changes: 23 additions & 14 deletions examples/CRISP/server/src/server/repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,7 @@ impl<S: DataStore> CrispE3Repository<S> {
has_voted: vec![],
start_time: 0u64,
status: "Requested".to_string(),
votes_option_1: "0".to_string(),
votes_option_2: "0".to_string(),
tally: vec![],
emojis: generate_emoji(),
token_holder_hashes: vec![],
eligible_addresses: vec![],
Expand All @@ -199,6 +198,11 @@ impl<S: DataStore> CrispE3Repository<S> {
Ok(e3)
}

pub async fn get_num_options(&self) -> Result<usize> {
let e3_crisp = self.get_crisp().await?;
Ok(e3_crisp.num_options.parse::<usize>()?)
}

pub async fn get_vote_count(&self) -> Result<u64> {
let e3_crisp = self.get_crisp().await?;
Ok(u64::try_from(e3_crisp.has_voted.len())?)
Expand All @@ -219,19 +223,25 @@ impl<S: DataStore> CrispE3Repository<S> {
Ok(())
}

pub async fn set_votes(&mut self, option_1: BigUint, option_2: BigUint) -> Result<()> {
info!("set_votes(option_1:{} option_2:{})", option_1, option_2);
pub async fn set_votes(&mut self, votes: Vec<BigUint>) -> Result<()> {
info!(
"set_votes: [{}]",
votes.iter().enumerate()
.map(|(i, v)| format!("option_{}: {}", i, v))
.collect::<Vec<_>>()
.join(", ")
);

let key = self.crisp_key();
self.store
.modify(&key, |e3_obj: Option<E3Crisp>| {
e3_obj.map(|mut e| {
e.votes_option_1 = option_1.to_string();
e.votes_option_2 = option_2.to_string();
e
})
.modify(&key, |e3_obj: Option<E3Crisp>| {
e3_obj.map(|mut e| {
e.tally = votes.iter().map(|v| v.to_string()).collect();
e
})
.await
.map_err(|_| eyre::eyre!("Could not append ciphertext_input for '{key}'"))?;
})
.await
.map_err(|_| eyre::eyre!("Could not set votes for '{key}'"))?;
Ok(())
}

Expand All @@ -250,8 +260,7 @@ impl<S: DataStore> CrispE3Repository<S> {
let e3_crisp = self.get_crisp().await?;
Ok(WebResultRequest {
round_id: e3.id,
option_1_tally: e3_crisp.votes_option_1,
option_2_tally: e3_crisp.votes_option_2,
tally: e3_crisp.tally,
option_1_emoji: e3_crisp.emojis[0].clone(),
option_2_emoji: e3_crisp.emojis[1].clone(),
end_time: e3.expiration,
Expand Down
Loading
Loading